Un cargador Stage-0 en Rust para Crystal Palace PICOs

2026-08-06 — Research

Estaba harto de escribir VirtualAllocWriteProcessMemoryCreateRemoteThread en cada stage y de ver cómo las mismas reglas YARA lo detectaban cada vez. Así que lo construí yo mismo.

Es un cargador Stage-0 en Rust. Su tarea es sencilla: transporta un PICO encriptado de Mythic/Xenon Crystal Palace dentro de su propio PE, lo descifra en memoria, inicia un proceso de Windows benigno, modifica ETW y AMSI en el objetivo, inyecta el PICO y se cierra. No llama a CreateProcessW. No llama a GetModuleHandleW. No importa OpenProcess. Cada llamada a sistema pasa por una tabla indirecta de 16 entradas recuperada de un ntdll limpio — el método Tartarus Gate con la recuperación de stubs de Halo, porque me encontré con stubs modificados en la primera prueba real y tuve que añadir ese paso de recuperación.

Este artículo trata sobre su arquitectura. Sin código.


0.0. ¿Porque en rust?.

Porque me daba la gana y me apetecía. no hay una explicación súper técnica ni nada por el estilo. Al final es otro lenguaje de bajo nivel como C y C++. tiene un winapi bien documentada y una ntapi decentemente documentada.Además quería estudiarme el repositorio de Smukx que tiene muchos ejemplos para creación de malware y llamadas de sistemas en rust. Aquí el enlace el repositorio: https://git.smukx.site/smukx/Rust-for-Malware-Development

0.1 ¿como lo e desarroyado?

Como ya he mencionado, he utilizado mayormente un el repo de smukx. También al final el conocimiento del CRTL, CETP y maldev se puede aplicar en casi todos los lenguajes. Lo que nunca voy a negar es que a mí me gusta emplear IA para mis desarrollos. Yo en el apartado de talks de mi web (si lees esto en gitbook alomejor no lo encuentras, es un apartado de mi web donde también publico mis investigaciones y blogs) ya explique mi posición. Y sinceramente desarrollar esto desde 0 con un lenguague que no domino tanto como es rust, habría tardado fácilmente 4 meses o más. Y con la IA, mis conocimientos y mis apuntes lo he podido hacer en 3 semanas un mes. Si lo hubiera hecho en C si que es cierto que como lo domino muchisimo mas habria tardado menos y incluso no usar la IA (aúna si la habría usado. es una herramienta que me gusta para escribir código). Pero eso, yo lo digo, mi posición es esta y ya está.

0.2 Funcionamiento.

Loader Image

es la misma foto que sale en la publicacion de crystal palace. no tiene mucho mas.

1. Qué ocurre antes de que se ejecute el compilador de Rust

builder.py toma el PICO en texto plano .bin, lo cifra con una clave AES-256-CTR aleatoria y codifica cada byte mediante palabra: cada uno se mapea a una palabra inglesa de dos o tres letras. ¿Por qué? Porque una sección de PE llena de texto cifrado brilla como “carga útil encriptada” ante cualquier analizador de entropía. Un bloque de palabras separadas por espacios en blanco del Scrabble no genera esa misma señal.

El diccionario de 256 palabras mezclado, la clave aleatoria, el nonce y el texto cifrado codificado se escriben como constantes en tiempo de compilación en src/payload.rs. El generador ejecuta un ciclo de descifrado y aborta si los bytes no coinciden. Solo olvidé esa comprobación una vez.

En tiempo de compilación, build.rs hace dos cosas. Asamblea hellsgate.asm con NASM: el trampolín que almacena el SSN y salta al gadget syscall; ret en ntdll. Además, ejecuta x86_64-w64-mingw32-windres sobre un archivo de recursos que contiene un ícono de Chrome y un bloque VERSIONINFO que afirma que el binario final es Google Chrome 123.0.6312.86. Ese objeto se enlaza dentro del PE. El binario incluye metadatos de Chrome. Es un truco simple, pero no cuesta nada y hace que los strings generados sean un poco menos llamativos.

Las versiones de lanzamiento utilizan opt-level = 3, lto = true, codegen-units = 1 y panic = "abort". Hay un strip manual al final: porque strip = true de Cargo está defectuoso en x86_64-pc-windows-gnu y elimina la sección .rdata, que es justo donde reside la carga útil encriptada. Eso me costó una hora.


2. Qué ocurre en tiempo de ejecución

La cadena de procesos es estricta. Cada mensaje de diagnóstico está protegido por la bandera debug y se compila en nada en producción. Si deseas salida detallada, compila con --features debug. De lo contrario, el binario permanece silencioso.

Desacoplamiento de la consola. Primero se ejecuta FreeConsole(). No hay ventana visible.

Anti-debugging en PEB. Se lee la bandera BeingDebugged en gs:[0x60] + 0x2 mediante ensamblador en línea. Si hay un depurador conectado, se ejecuta exit(0). Sin mensaje.

Tabla de llamadas a sistema. Este es el paso más costoso. El cargador recorre ntdll.dll a través del PEB. Analiza el directorio de exportaciones, calcula el hash de cada nombre Zw* con FNV-1a y lo compara con los 16 nombres de llamada a sistema hasheados en tiempo de compilación. Cada SSN se extrae del stub limpio: el valor inmediato mov eax, <ssn>. Si el stub está modificado (un jmp en la posición 0 o 3), el paso de recuperación recorre los stubs adyacentes en pasos de 32 bytes. Dado que los stubs de ntdll están secuenciales y los SSN difieren en uno por stub, se puede inferir el SSN de un stub modificado a partir de uno limpio cercano. El gadget syscall; ret (0x0F 0x05 0xC3) se localiza escaneando el .text de ntdll.

Las 16 llamadas a sistema de la tabla son: NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory, NtCreateThreadEx, NtQueueApcThread, NtAlertResumeThread, NtTraceEvent, NtWaitForSingleObject, NtFreeVirtualMemory, NtQuerySystemInformation, NtQueryInformationProcess, NtSetInformationVirtualMemory, NtOpenProcess, NtGetContextThread, NtDelayExecution y NtTerminateProcess.

ProcessDebugPort. Se consulta NtQueryInformationProcess(ProcessDebugPort) a través de la puerta indirecta. -1 indica la presencia de un depurador. Se cierra el proceso.

Retraso de sandbox. Entre 5 y 15 segundos, valor aleatorio, mediante NtDelayExecution. El generador de números aleatorios es un xorshift64* sembrado con RDTSC. Sin importar ninguna API de temporización. Esto supera a los sandbox con tiempos de espera cortos que matan los procesos después de unos segundos. Simple, pero efectivo.

Verificaciones opcionales. Dos funciones, ambas desactivadas por defecto. anti-vm comprueba CPUID, detección de hipervisores y búsquedas en el registro. dacl establece una SDDL restrictiva en el handle del proceso actual: denegar a Todos, permitir a SYSTEM y al propietario. Las dejo desactivadas en producción porque añaden strings e importaciones.

Descifrado. El texto cifrado codificado en palabras se descifra mezclando el diccionario de 256 palabras con el mismo generador de números aleatorios pseudoaleatorios (LCG) que utilizó el generador. Cada palabra se convierte de nuevo en un byte. Se descifra el resultado con AES-256-CTR. El PICO en texto plano es un Vec<u8> temporal: nunca se escribe en disco y desaparece cuando termina la función.

Creación del proceso objetivo. NtCreateUserProcess crea el proceso al que se inyectará. El valor predeterminado es RuntimeBroker.exe: un proceso legítimo y firmado por Microsoft en Windows 10 y 11. Si se solicitó spoofing de PPID, NtOpenProcess abre el proceso padre y se establece PS_ATTRIBUTE_PARENT_PROCESS en los atributos de creación. La ruta del archivo e la línea de comandos se construyen con RtlInitUnicodeString y RtlCreateProcessParametersEx. No hay rastro de CreateProcessW en ningún lugar.

Modificación remota de ETW. ntdll!NtTraceEvent se localiza mediante el PEB y el directorio de exportaciones. En la posición NtTraceEvent + 3 del proceso objetivo se escriben cuatro bytes: xor eax, eax; nop; ret (0x33 0xC0 0x90 0xC3). La función devuelve STATUS_SUCCESS de inmediato. Los proveedores de ETW en usuario dejan de registrar datos. Esto funciona porque ntdll se carga en la misma dirección base en todos los procesos dentro de la misma sesión de Windows. Esa suposición se cumple en la práctica, pero es una suposición: si el proceso objetivo tiene una dirección base de ntdll diferente, la modificación fallará silenciosamente. No he visto que ocurra, pero existe esa posibilidad.

Modificación remota de AMSI. Si amsi.dll está mapeado en el proceso cargador, se localiza AmsiScanBuffer. Se crea un hilo remoto en LoadLibraryA("amsi.dll") para introducir AMSI en el proceso objetivo. El cargador espera mediante NtWaitForSingleObject. Luego se modifica AmsiScanBuffer para que sea mov eax, 0x8007000E; ret (0xB8 0x0E 0x00 0x07 0x80 0xC3): cada análisis devuelve E_OUTOFMEMORY. El escáner de scripts de Windows Defender queda ciego dentro del proceso objetivo.

Ambas modificaciones pasan por NtProtectVirtualMemoryNtWriteVirtualMemoryNtProtectVirtualMemory (restablecimiento), todo a través de la puerta indirecta.

Inyección. NtAllocateVirtualMemory asigna memoria RWX en el proceso objetivo. Utilizo PAGE_EXECUTE_READWRITE en lugar de realizar una transición gradual de RW a RX, porque el PICO de Crystal Palace necesita páginas de código escritibles durante su propia inicialización: bajar a RX provocaría su colapso. NtWriteVirtualMemory copia el PICO descifrado. NtCreateThreadEx inicia la ejecución en la posición cero de la asignación remota. +gofirst de Crystal Palace garantiza que go() se encuentre allí.

Limpieza. Todos los handles se cierran con NtClose. Si algún paso falla, NtTerminateProcess mata al proceso objetivo: no queda ningún proceso suspendido que pueda levantar sospechas. Se cierra el proceso.


3. Stack de evasión, resumido

Capa Función
En disco Recurso de VERSIONINFO de Chrome, strings sensibles cifradas con obfstr, carga útil codificada en palabras (baja entropía)
Resolución de APIs Recorrido del PEB en lugar de GetModuleHandleW/GetProcAddress; nombres de llamadas a sistema hasheados con FNV-1a
Llamadas a sistema Tabla indirecta de 16 entradas Tartarus Gate, recuperación de stubs de Halo
Creación de procesos NtCreateUserProcess con spoofing opcional de PPID mediante PS_ATTRIBUTE_PARENT_PROCESS
Anti-análisis Bandera PEB.BeingDebugged + NtQueryInformationProcess(ProcessDebugPort); retraso de sandbox aleatorio de 5–15 segundos
Modificaciones remotas ETW: 0x33 0xC0 0x90 0xC3 en NtTraceEvent+3. AMSI: 0xB8 0x0E 0x00 0x07 0x80 0xC3 en AmsiScanBuffer
Opcionales anti-vm (CPUID + registro), dacl (SDDL de autoprotección)

4. Qué sigue en la tabla de importaciones

El binario de lanzamiento no importa ninguno de los componentes habituales: no hay OpenProcess, CreateProcessW, WriteProcessMemory, VirtualAllocEx, VirtualProtectEx, CreateRemoteThread, ni ADVAPI32.dll. Las importaciones esenciales son exportaciones nativas de ntdll: NtCreateUserProcess, NtOpenProcess, NtClose, NtQueryInformationProcess, NtQueryInformationThread, y las funciones Rtl* relacionadas con el heap y los parámetros. Las únicas importaciones restantes de KERNEL32.dll son de la CRT en tiempo de ejecución: HeapAlloc, TlsAlloc, Sleep, GetLastError, FreeConsole.

Hay una importación de la que no puedo deshacerme sin implementar una versión no_std: CreateToolhelp32Snapshot. La biblioteca estándar de Rust y la CRT de MinGW la incluyen sin importar. El cargador nunca la llama, pero permanece en la IAT. Si alguien escribe una regla YARA para ella, el binario se activará. Una versión no_std solucionaría este problema.


5. Cosas que podrían mejorarse

si que es cierto que tengo cosas a mejorar. quiero terminar algunas de las ultimas tecnicas de para alogar el loader en memoria de manera mas segura y tambien tengo que seguir con las pruebas del anti debug y VM. estoy probando unas cositas para que al detectar un por ejemplo IDA pro que auto destrulla el sistema entero. Un poco kamikaze pero no quiero que analicen mis cosas si no soy yo :´)


El cargador tiene un único propósito: descifrar, iniciar, modificar, inyectar y cerrarse. Todas las APIs Win32 de alto nivel han desaparecido. La evasión es multicapa: en tiempo de compilación, en tiempo de carga y en tiempo de ejecución, y cada capa se puede mejorar sin afectar a las demás. No es indetectable. Nada lo es. Pero hace que el analista trabaje más duro, y de eso se trata todo.


Referencias

[1] Cody Thomas, "Mythic C2 Framework Documentation," docs.mythic-c2.net, 2026. https://docs.mythic-c2.net/home

[2] c0rnbread, "Xenon: Cobalt Strike-like Windows Agent for Mythic," GitHub, 2024-2026. https://github.com/MythicAgents/Xenon

[3] c0rnbread, "Xenon Wiki Evasion and User-Defined Reflective Loaders," MythicAgents Wiki. https://github.com/MythicAgents/Xenon/wiki/Evasion

[4] c0rnbread, "Crystal-Kit-Xenon: Crystal Kit Compatible with Mythic Xenon Agent," GitHub, 2024-2026. https://github.com/nickswink/crystal-kit-xenon

[5] Raphael Mudge, "Crystal Palace Documentation and Linker Script Language," Tradecraft Garden, 2025-2026. https://tradecraftgarden.org/crystalpalace.html

[6] RastaMouse, "Crystal-Kit: Cobalt Strike Evasion with Crystal Palace," GitHub, 2024-2026. https://github.com/rasta-mouse/Crystal-Kit

[7] Lorenzo Meacci, "Bypassing EDR in a Crystal Clear Way KaplaStrike," lorenzomeacci.com, 2026. https://lorenzomeacci.com/bypassing-edr-in-a-crystal-clear-way

[8] RastaMouse, "GadgetHunter Call Stack Spoofing Gadget Scanner," GitHub, 2024. https://github.com/rasta-mouse/GadgetHunter

[9] MythicMeta, "Mythic Community Overview Agents and C2 Profiles." https://mythicmeta.github.io/overview/

[10] Companion post: "Crystal Palace: The Linker That Changes Everything."

[11] https://git.smukx.site/smukx/Rust-for-Malware-Development

Esta publicación tiene fines educativos y de investigación únicamente. Las técnicas descritas solo deben utilizarse en entornos autorizados con permiso explícito.

← back to blog