No sea así y quédese: evitar la ejecución de Fork&Run .NET con InlineExecute-Assembly

Un hombre mirando la pantalla de una computadora mientras trabaja tarde en la noche escribiendo código

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.

Antecedentes

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.

Las últimas noticias tecnológicas, respaldadas por los insights de expertos

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.

¡Gracias! Ya está suscrito.

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.

¿Por qué InlineExecute-Assembly?

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.

Mixture of Experts | 12 de diciembre, episodio 85

Decodificación de la IA: Resumen semanal de noticias

Únase a nuestro panel de ingenieros, investigadores, responsables de producto y otros profesionales de talla mundial que se abren paso entre el revuelo de la IA para ofrecerle las últimas noticias e insights al respecto.

Características clave

Cargar el Common Language Runtime (CLR)

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.

Captura de pantalla Cargar el CLR

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:

  1. Hace una llamada a CLRCreateInstance que se usará para recuperar nuestra interfaz ICLRMetaHost.
  2. Luego se usa ICLRMetaHost - > GetRunTime para obtener la información del tiempo de ejecución para la versión de.NET que solicitamos. Si el ensamblaje se creó con .NET versión 3.5 o inferior, debemos solicitar v2.0.50727, y si el ensamblaje se creó con .NET 4.0 y superior, debemos solicitar v4.0.30319. En realidad, hay una función en el BOF que nos ayudará a averiguar qué versión usa automáticamente nuestro ensamblaje .NET, pero hablaremos de eso más adelante.
  3. Una vez que tenemos nuestra información de tiempo de ejecución, utilizamos ICLRRuntimeInfo->IsLoadable para comprobar si nuestro tiempo de ejecución se puede cargar en el proceso. Esto también tomará en cuenta si ya se han cargado otros tiempos de ejecución y establecerá nuestro valor BOOL fLoadable en 1 (verdadero) si nuestro tiempo de ejecución se puede cargar en el proceso.
  4. Si todo está bien, ejecutaremos ICLRRuntimeInfo->GetInterface para cargar el CLR en nuestro proceso y recuperar una interfaz para ICorRunTimeHost.
  5. Por último, llamaremos a ICorRuntimeHost->Start, que inicia el CLR.

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.

Captura de pantalla: AppDomain que se crea y el ensamblaje se carga/ejecuta

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:

  1. Utilice ICorRuntimeHost->CreateDomain para crear nuestro AppDomain único.
  2. Utilice IUnknown->QueryInterface (pAppDomainThunk) para obtener un puntero a la interfaz AppDomain
  3. Cree nuestro SafeArray y copie nuestros bytes de ensamblaje .NET en él
  4. Cargue nuestro ensamblaje a través de AppDomain->Load_3
  5. Obtenga nuestro punto de entrada en nuestro ensamblaje a través de Assembly->EntryPoint
  6. Por último, invoque nuestro ensamblaje a través de MethodInfo->Invoke_3 

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.

Redirigir STDOUT de la consola al pipe con nombre o ranura de correo: evitar la modificación de herramientas

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:

Captura de pantalla: Redirigir la salida de la consola estándar al pipe con nombre y revertir el cambio

Redirigir la salida de la consola estándar al pipe con nombre y revertir el cambio

Determine la versión .NET del ensamblado

¿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:

Captura de pantalla: función que dice nuestro ensamblaje .NET y ayuda a determinar qué versión de .NET necesitamos al cargar el CLR

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:

Captura de pantalla: declaración If/else para establecer la variable de versión .NET

Declaración if/else para establecer la variable de versión .NET

Aplicación de parches a la interfaz de escaneo antimalware (AMSI)

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:

Captura de pantalla: parches en memoria de AmsiScanBuffer

Aplicación de parches en memoria de AmsiScanBuffer

Como puede ver en la captura de pantalla anterior, simplemente hacemos lo siguiente:

  1. Cargar amsi.dll y obtener un puntero a AmsiScanBuffer
  2. Cambiar la protección de la memoria
  3. Parchear en nuestros bytes amsiPatch[]
  4. Revertir la protección de la memoria a su estado original

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:

Captura de pantalla: Ejemplo de eludir AMSI de InlineExecute-Assemby

Ejemplo de eludir AMSI de InlineExecute-Assemby

Aplicación de parches a Event Tracing for Windows (ETW)

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:

Captura de pantalla: Aplicación de parches en memoria de EtwEventWrite

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:

Captura de pantalla: uso de Process Hacker para ver las propiedades de PowerShell.exe antes de ejecutar inlineExecute-Assembly con el indicador –etw

Uso de Process Hacker para ver las propiedades de PowerShell.exe antes de ejecutar inlineExecute-Assembly con el indicador –etw

Captura de pantalla: Ejecución de inline-Execute-Assembly con el indicador –etw

Ejecutar inline-Execute-Assembly con el indicador –etw

Captura de pantalla: uso de Process Hacker para ver las mismas propiedades de PowerShell.exe después de ejecutar inlineExecute-Assembly

Uso de Process Hacker para ver las mismas propiedades de PowerShell.exe después de ejecutar inlineExecute-Assembly

AppDomains, Named Pipes, Mail Slots únicos

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:

Captura de pantalla de terminal que muestra la ejecución del comando inlineExecute-Assembly --dotnetassembly /root/Desktop/MessageBoxCS.exe en Beacon. La salida incluye mensajes de estado: running inlineExecute-Assembly, host called home (sent 16319 bytes), received output ‘Hello From .NET!’, and completion message ‘inlineExecute-Assembly Finished’.

Ejemplo de InlineExecute-Assembly usando un AppDomain Name único y un nombre de pipe named único

Captura de pantalla: Ejemplo de un nombre único de AppDomain ChangedMe

Ejemplo de nombre único de AppDomain ChangedMe

Captura de pantalla: Ejemplo de named pipe único LookAtMe

Ejemplo de named pipe único LookAtMe

Captura de pantalla: ejemplo de la eliminación de AppDomain después de que finaliza la ejecución exitosa

Ejemplo de eliminación de AppDomain después de que finaliza la ejecución exitosa

Captura de pantalla: ejemplo de eliminación de un named pipe después de que finaliza la ejecución exitosa

Ejemplo de eliminación de un named pipe tras finalizar correctamente la ejecución

Advertencias

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:

  1. Aunque he intentado que sea lo más estable posible, no hay garantías de que nunca se vaya a bloquear ni de que las balizas no vayan a dejar de funcionar. No tenemos el lujo adicional de hacer fork and rn¿un donde, si algo sale mal, vive nuestra baliza. Esta es la contrapartida de los BOF. Dicho esto, no puedo dejar de insistir en lo importante que es que pruebes sus ensamblajes de antemano para asegurarse de que funcionarán correctamente.
  2. Dado que BOF se ejecuta en el proceso y se hace cargo de su baliza mientras se ejecuta, esto debe tenerse en cuenta antes de usarse para ensamblajes de larga duración. Si elige ejecutar algo que tardará mucho tiempo en obtener resultados, su baliza no estará activa para ejecutar más comandos hasta que vuelvan los resultados y su ensamblaje termine de ejecutarse. Esto tampoco se ajusta a la suspensión. Por ejemplo, si su suspensión se establece en 10 minutos y ejecuta el BOF, obtendrá resultados tan pronto como el BOF termine de ejecutarse.
  3. A menos que se realicen modificaciones en las herramientas que cargan PE en la memoria (por ejemplo, SafetyKatz), lo más probable es que destruyan su baliza. Muchas de estas herramientas funcionan bien con el ensamblaje de ejecución porque pueden enviar la salida de su consola desde el proceso de sacrificio antes de salir. Cuando salen a través de nuestro BOF en proceso, matan nuestro proceso, lo que mata nuestra baliza. Estos se pueden modificar para que funcionen, pero recomendaría ejecutar este tipo de ensamblajes mediante el ensamblaje de ejecución, ya que se podrían cargar en su proceso otras cosas no compatibles con OPSEC que no se eliminan.
  4. Si su ensamblaje utiliza Environment.Exit, será necesario quitarlo ya que matará el proceso y la baliza.
  5. Los named pipes y los mail slots deben ser únicos. Si no recibe datos y su baliza aún está activa, lo más probable es que deba seleccionar un named pipe diferente o un nombre diferente de mail slot.

Consideraciones defensivas

A continuación se presentan algunas consideraciones defensivas:

  1. Esto utiliza PAGE_EXECUTE_READWRITE al aplicar parches de memoria a AMSI y ETW. Esto se hizo a propósito y debería ser una señal de alerta, ya que muy pocos programas tienen rangos de memoria con la protección de memoria de PAGE_EXECUTE_READWRITE.
  2. El nombre predeterminado del named pipe creado es "totesLegit". Esto se hizo a propósito y las detecciones de firmas podrían usarse para marcar esto.
  3. El nombre predeterminado del mail slot creado es "totesLegit". Esto se hizo a propósito y las detecciones de firmas podrían usarse para marcar esto.
  4. El nombre predeterminado del AppDomain cargado es "totesLegit". Esto se hizo a propósito y las detecciones de firmas podrían usarse para marcar esto.
  5. Buenos consejos para detectar el uso malicioso de .NET (por @bohops) aquí, (por F-Secure) aquí y aquí.
  6. Buscar la carga de .NET CLR en procesos sospechosos, como procesos no gestionados que nunca deberían tener cargado el CLR.
  7. Más sobre seguimiento de eventos.
  8. Buscar otros IOC conocidos de Cobalt Strike Beacon o IOC de salida/comunicación C2.