A algunos de ustedes les encanta y a otros lo odian, pero en este punto no debería sorprender que .NET tradecraft haya llegado para quedarse un poco más de lo previsto. .NET es una parte integral del sistema operativo de Microsoft y la versión más reciente de .NET es .NET core. Core es el sucesor multiplataforma de .NET Framework que también lleva .NET a Linux y macOS. Esto hace que .NET sea ahora más popular que nunca para el tradecraft posterior a la explotación entre adversarios y equipos rojos. Este blog profundizará en un nuevo Beacon Object File (BOF) que permite a los operadores ejecutar ensamblajes .NET en proceso mediante Cobalt Strike, en lugar del módulo tradicional de ejecutar-ensamblar integrado, que usa la técnica fork and run.
Cobalt Strike, un popular software de simulación de adversarios, reconoció la tendencia de los equipos rojos a alejarse de las herramientas de PowerShell en favor de C# debido al aumento de la capacidad de detección de PowerShell, y en 2018 con Cobalt Strike versión 3.11, introdujo el módulo de ejecución y ensamblaje. Esto permitió a los operadores usar el poder de los ensamblajes .NET posterior a la explotación ejecutándolos en la memoria sin tener el riesgo adicional de colocar esas herramientas en el disco. Si bien la capacidad de cargar ensamblajes .NET en la memoria a través de código no administrado no era nueva ni desconocida en el momento del lanzamiento, diría que Cobalt Strike generalizó la capacidad y ayudó a continuar impulsando la popularidad de .NET para el tradecraft posterior a la explotación.
El módulo de ejecución y ensamblaje de Cobalt Strike utiliza la técnica de fork and run, que consiste en generar un nuevo proceso de sacrificio, inyectar su código malicioso posterior a la explotación en ese nuevo proceso, ejecutar su código malicioso y, cuando termine, eliminar el nuevo proceso. Esto tiene tanto sus beneficios como sus inconvenientes. El beneficio del método fork and run es que la ejecución ocurre fuera de nuestro proceso de implante de Beacon. Esto significa que si algo en nuestra acción posterior a la explotación sale mal o se detecta, hay muchas más posibilidades de que nuestro implante sobreviva. Para simplificar, realmente ayuda con la estabilidad general del implante. Sin embargo, debido a que los proveedores de seguridad se dieron cuenta de este comportamiento de fork and run, ahora se ha agregado lo que admite Cobalt Strike, un patrón costoso de OPSEC.
A partir de la versión 4.1 lanzada en junio de 2020, Cobalt Strike introdujo una nueva característica para tratar de ayudar a resolver este problema con la introducción de Beacon Object Files (BOF). Los BOF permiten a los operadores evitar los conocidos patrones de ejecución descritos anteriormente u otras fallas de OPSEC, como el uso de cmd.exe/powershell.exe mediante la ejecución de archivos de objetos en la memoria dentro del mismo proceso que nuestro implante de Beacon. Si bien no entraré en el funcionamiento interno de los BOF, estas son algunas entradas en el blog que encontré interesantes:
Si leyó los blogs anteriores, ahora deberíamos ver que los BOF no fueron exactamente lo que esperábamos y si soñaba con reescribir todas esas increíbles herramientas .NET y convertirlas en BOF, esos sueños ahora se han roto. Lo siento. Sin embargo, la esperanza no se pierde, ya que, en mi opinión, hay algunas cosas excelentes que los BOF pueden ofrecer, y recientemente me he divertido mucho (y también frustado un poco) superando los límites de lo que se puede hacer con ellos. Primero, fue creando CredBandit, que realiza un volcado completo en memoria de un proceso como LSASS y lo envía de vuelta a través de su canal de comunicación Beacon existente. Hoy voy a lanzar InlineExecute-Assembly, que se puede usar para ejecutar ensamblajes .NET dentro de su proceso beacon sin modificar su herramienta favorita de .NET. Analicemos por qué escribí el BOF, algunas de sus características clave, advertencias y cómo podría ser útil al realizar simulaciones de adversarios o equipos rojos.
Boletín de la industria
Manténgase al día sobre las tendencias más importantes e intrigantes de la industria sobre IA, automatización, datos y más con el boletín Think. Consulte la Declaración de privacidad de IBM.
Su suscripción se entregará en inglés. En cada boletín, encontrará un enlace para darse de baja. Puede gestionar sus suscripciones o darse de baja aquí. Consulte nuestra Declaración de privacidad de IBM para obtener más información.
La razón detrás de la creación de InlineExecute-Assembly es bastante simple. Quería una forma para que nuestro equipo de simulación de adversarios ejecutara ensamblajes .NET en proceso para evitar algunos de esos errores de OPSEC mencionados anteriormente al usar Cobalt Strike para operar en entornos maduros. También necesitaba la herramienta para no sobrecargar a nuestro equipo con tiempo de desarrollo adicional al tener que realizar modificaciones en la mayoría de nuestras herramientas .NET actuales. También necesitaba ser estable. Bueno, tan estable como puede ser un BOF complejo, ya que lo último que queremos es perder una de nuestras pocas Beacons en el entorno. Básicamente, debería funcionar tan bien para el operador como el módulo de ejecución y ensamblaje de Cobalt Strike como sea posible.
Lo sé, es bastante obvio. No llegaríamos muy lejos sin él, ¿verdad? Fuera de broma, las complejidades de cómo funciona el CLR y lo que sucede en profundidad podrían ser una entrada en el blog en sí misma, por lo que vamos a revisar lo que utiliza el BOF a un nivel muy alto al cargar el CLR mediante código no administrado.
Cargar el CLR
Como se muestra en la captura de pantalla simplificada anterior, los pasos principales que tomará el BOF para cargar el CLR son los siguientes:
Así que ahora el CLR está inicializado, pero aún queda un poco más por hacer antes de que podamos ejecutar nuestros ensamblajes .NET favoritos. Necesitamos crear nuestra instancia de AppDomain, que es lo que Microsoft explica como "un entorno aislado donde se ejecutan las aplicaciones". En otras palabras, esto se utilizará para cargar y ejecutar nuestros ensamblajes .NET posteriores a la explotación.
Se está creando un AppDomain que se crea y el ensamblaje se carga/ejecuta
Como se muestra en la captura de pantalla simplificada anterior, los pasos principales que tomará el BOF para cargar e invocar nuestro ensamblaje .NET son los siguientes:
Esperamos que ahora tenga un entendimiento general de la ejecución de .NET mediante código no gestionado, pero esto aún no nos acerca a tener una herramienta operativamente estable, así que veremos algunas características que se implementaron en BOF para pasar de regular a totalmente legítimo.
Probablemente se esté preguntando por qué esto es importante. Bueno, si es como yo y valora su tiempo, no querrá gastarlo modificando casi todos los ensamblajes .NET para que su punto de entrada devuelva una cadena con todos sus datos que normalmente solo se canalizarían a la salida de consola estándar, ¿verdad? Ya me lo imaginaba. Para evitar eso, lo que debemos hacer es redirigir nuestra salida estándar a un pipe con nombre o a una ranura de correo, leer la salida después de haberla escrito y luego revertirla a su estado original. De esta manera, podemos ejecutar nuestros ensamblajes no modificados como lo haríamos desde cmd.exe o powershell.exe. Ahora, antes de repasar cualquier código, debo dar las gracias a @N4k3dTurtl3 y su entradaen el blog sobre el ensamblador en proceso y las ranuras de correo. Esto es lo que originalmente me llevó a implementar esta técnica en mi propio implante C privado cuando salió por primera vez y muchos meses después transferí esa misma funcionalidad a un BOF. Bien, ahora que se han dado los accesorios, veamos un ejemplo simplificado de cómo se lograría esto redirigiendo stdout a un pipe con nombre a continuación:
Redirigir la salida de la consola estándar al pipe con nombre y revertir el cambio
¿Recuerda que al cargar el CLR a través de ICLRMetaHost ->GetRuntime tuvimos que especificar qué versión de la infraestructura .NET necesitábamos? ¿Recuerda que eso depende de la versión con la que se compiló nuestro ensamblaje .NET? No sería muy divertido tener que especificar manualmente qué versión se necesita cada vez, ¿verdad? Por suerte para nosotros, @b4rtik implementó una función interesante para gestionar esto en su módulo de ensamblaje ejecutado para la infraestructura Metasploit que podemos implementar fácilmente en nuestras propias herramientas que se muestran a continuación:
Función que dice nuestro ensamblaje .NET y ayuda a determinar qué versión de .NET necesitamos al cargar el CLR
Esencialmente, lo que hace esta función es que cuando pasa nuestros bytes de ensamblaje, lee esos bytes y busca los valores hexadecimales de 76 34 2E 30 2E 33 30 33 31 39, que cuando se convierte a ASCII es v4.0.30319. Espero que le resulte familiar. Si se encuentra ese valor al leer el ensamblaje, la función devuelve 1 o verdadero, y si no se encuentra, devuelve 0 o falso. Podemos usar esto para determinar fácilmente con qué versión cargar si 1/verdadero o 0/falso regresa como se muestra en el ejemplo de código a continuación:
Declaración if/else para establecer la variable de versión .NET
Definitivamente no podíamos hablar de técnicas ofensivas de .NET y no mencionar a AMSI. Si bien no profundizaremos en qué es AMSI y todas las formas en que se puede omitir, ya que esto se ha tratado muchas veces, hablaremos un poco sobre por qué puede ser necesario parchear AMSI dependiendo de lo que decida ejecutar a través del BOF. Por ejemplo, si decide ejecutar Seatbelt sin ningún tipo de ofuscación, notará rápidamente que no obtienes ningún resultado y que su baliza está muerta. Sí, muerta. Esto se debe a que AMSI detectó su ensamblaje, determinó que era malicioso y lo desactivó, como si se tratara de una fiesta en una casa que hace demasiado ruido. No es ideal, ¿verdad? Ahora tenemos dos buenas opciones aquí cuando se trata de AMSI; podemos ofuscar nuestras herramientas.NET a través de algo como ConfuserX o Invisibility Cloak o podemos deshabilitar AMSI usando una variedad de técnicas. En nuestro caso, utilizaremos uno de RastaMouse, que consiste en parchear amsi.dll en la memoria para que devuelva E_INVALIDARG y hace que el resultado del análisis sea 0. Lo que, como se señala en su entrada en el blog, suele interpretarse como AMSI_RESULT_CLEAN. Veamos a continuación una versión simplificada del código para un proceso x64:
Aplicación de parches en memoria de AmsiScanBuffer
Como puede ver en la captura de pantalla anterior, simplemente hacemos lo siguiente:
Al implementar esto en nuestra herramienta, ahora deberíamos poder ejecutar la versión predeterminada de Seatbelt.exe usando el indicador –amsi para evitar la detección de AMSI, como se muestra a continuación:
Ejemplo de eludir AMSI de InlineExecute-Assemby
Afortunadamente para los defensores, hay más que solo AMSI para ayudar cuando se trata de detectar tradecraft malicioso de .NET mediante ETW. Desafortunadamente, al igual que AMSI, esto también puede ser bastante fácil de eludir para los adversarios, y @xpn realizó una investigación realmente impresionante sobre cómo se podría hacer. Veamos un ejemplo simplificado de cómo se podrían aplicar parches a ETW para deshabilitarlo por completo a continuación:
Aplicación de parches en memoria de EtwEventWrite
Como puede ver en la captura de pantalla anterior, los pasos son prácticamente idénticos a cómo aplicamos parches a AMSI, por lo que no repasaré los pasos para este. Puede ver una captura de pantalla de antes y después de ejecutar el indicador –etw a continuación:
Uso de Process Hacker para ver las propiedades de PowerShell.exe antes de ejecutar inlineExecute-Assembly con el indicador –etw
Ejecutar inline-Execute-Assembly con el indicador –etw
Uso de Process Hacker para ver las mismas propiedades de PowerShell.exe después de ejecutar inlineExecute-Assembly
De forma predeterminada, el AppDomain, Named Pipe o Mail Slot creado utiliza el valor predeterminado "totesLegit". Estos valores se pueden cambiar para integrarse mejor en el entorno que está probando cambiándolos en el script de agresor proporcionado o mediante indicadores de línea de comandos sobre la marcha. A continuación se muestra un ejemplo de cómo cambiarlas a través de la línea de comandos:
Ejemplo de InlineExecute-Assembly usando un AppDomain Name único y un nombre de pipe named único
Ejemplo de nombre único de AppDomain ChangedMe
Ejemplo de named pipe único LookAtMe
Ejemplo de eliminación de AppDomain después de que finaliza la ejecución exitosa
Ejemplo de eliminación de un named pipe tras finalizar correctamente la ejecución
Esta sección será más o menos una repetición de lo que mencioné en el repositorio de GitHub, pero sentí que era importante reiterar algunas cosas que debe tener en cuenta al usar esta herramienta:
A continuación se presentan algunas consideraciones defensivas: