Ser un buen host de CLR: modernizar el espionaje ofensivo de .NET

Mujer escribiendo en su ordenador dentro de un cuarto oscuro

El equipo rojo moderno se define por su capacidad para comprometer los endpoints y tomar medidas para completar los objetivos. Para lograr lo primero, muchos equipos implementan su propio comando y control personalizado (C2) o utilizan una opción de código abierto. Para esto último, hay un flujo constante de herramientas posteriores a la explotación que se benefician de varias características en Windows, Active Directory y aplicaciones. Durante los últimos años, el mecanismo de ejecución de estas herramientas se ha basado en gran medida en la ejecución de ensamblados .NET en memoria.

A pesar de ser una parte muy importante del arsenal moderno del equipo rojo, la técnica para ejecutar ensamblados de .NET en un endpoint comprometido se ha mantenido prácticamente estancado. En esta entrada de blog, hablaremos de cómo los equipos rojos pueden incorporar sus arneses de ejecución en .NET en esta década.

Puntos clave

  • Los operadores pueden tomar el control de muchos aspectos del CLR utilizando “personalizaciones de CLR” al ejecutar ensamblados .NET en memoria
  • Asumir la gestión de memoria para el CLR permite a los operadores controlar y rastrear todas las asignaciones realizadas por el CLR, y también proporciona una forma sencilla de controlar los ensamblados que se están cargando en el proceso
  • La implementación de un gestor de carga de ensamblados personalizado permite eludir AMSI de manera novedosa utilizando únicamente la funcionalidad "prevista", sin necesidad de parches de bytes ni hackeo de procesos

Una breve historia de la ejecución de ensamblados

No hace mucho tiempo, muchos equipos rojos confiaban en PowerShell para las herramientas posteriores a la explotación. Cobalt Strike dio un paso para cambiar eso en 2018 al implementar el módulo execute-assembly. La ejecución del ensamblado generaría un proceso de sacrificio e inyectaría una DLL reflexiva en la que cargaría el entorno en tiempo de ejecución de lenguaje común (CLR) y ejecutaría un ensamblado .NET proporcionado por el operador.

Esto provocó que muchas de las herramientas posteriores a la explotación se pasaran a los ensamblados de .NET. Después de un tiempo, los defensores comenzaron a crear detecciones para el comportamiento de "bifurcar y ejecutar" de la ejecución del ensamblado, es decir, la inyección reflexiva de DLL. Para solucionar este problema, Shawn Jones, también del equipo de IBM® Adversary Simulation, desarrolló el archivo de objeto de baliza (BOF) InlineExecute-Assembly. Esto permitió a los operadores alejarse del comportamiento de “bifurcar y ejecutar” de la ejecución del ensamblado y permanecer dentro del proceso de implantación. Desde entonces, muchos marcos C2 han adoptado este comportamiento de forma nativa.

Si aún no está familiarizado con cómo alojar el CLR y ejecutar ensamblados .NET, le recomendaría que lea la entrada de blog de Shawn enlazada arriba.

Mixture of Experts | 12 de diciembre, episodio 85

Descifrar 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 bullicio de la IA para ofrecerle las últimas noticias y conocimientos al respecto.

Cómo alojar el tiempo de ejecución de lenguaje común

La técnica de ejecución del ensamblado se beneficia de una característica de Windows conocida como “Alojamiento CLR no administrado”. El tiempo de ejecución de lenguaje común, o CLR, es el tiempo de ejecución para .NET. Los usuarios pueden escribir ensamblados .NET en una variedad de lenguajes (C#, F#, etc.) que se compilan en un lenguaje intermedio (IL). El CLR es responsable de tomar un ensamblado que contiene IL y ejecutarlo.

Tradicionalmente, la técnica de ejecución y ensamblaje se ha basado en el uso de la interfaz obsoletaICorRuntimeHost . La razón por la que los profesionales ofensivos utilizan esta interfaz es que permite cargar ensamblados desde un array de bytes en memoria, creando un dominio de aplicación y utilizando el método Load_3 . Al cargar desde un array de bytes, evitamos el problema de tener que colocar cualquier código en el sistema de archivos, que podría escanearse con soluciones defensivas.

Desde entonces, ICorRuntimeHost ha sido reemplazado por la interfaz ICLRRuntimeHost .

Página de MSDN para la interfaz ICLRRuntimeHost

Figura 1: página de MSDN para la interfaz ICLRRuntimeHost

Los documentos de MSDN para la interfaz ICLRRuntimeHost  afirman que añade el métodoSetHostControl , pero omite algunos métodos proporcionados por ICorRuntimeHost . Cuando Microsoft dice “omisión de algunos métodos”, se refiere a todos los métodos divertidos que nos permiten cargar ensamblados de forma reflexiva. A cambio, obtenemos acceso a las personalizaciones de CLR a través del método SetHostControl .

Nota: Aunque no podemos usar ICLRRuntimeHost  directamente para cargar ensamblados reflexivos, podemos iniciar el CLR usando ICLRRuntimeHost  y luego llamar a GetInterface  para obtener una interfaz ICorRuntimeHost . Entonces podemos usar la interfaz ICorRuntimeHost  para cargar conjuntos reflexivos y, al mismo tiempo, tener habilitadas nuestras personalizaciones de CLR.

¿Qué son las personalizaciones de CLR?

El método SetHostControl nos permite proporcionar nuestra propia implementación de la interfaz COM IHostControl , que es como podemos indicar al CLR que use varias características. Las personalizaciones de CLR son una característica de la que se habla poco y que permite a los desarrolladores tomar el control de aspectos del CLR. Las personalizaciones funcionan mediante el uso de varias interfaces de “gestor” que nosotros, como desarrolladores, podemos implementar, y nuestra implementación IHostControl indicará al CLR qué gestores nos gustaría que utilizara. Todo lo que no implementemos simplemente lo gestionará el CLR como normalmente lo haría. A continuación se muestra una lista de los gestores compatibles.

Algunas de las interfaces compatibles con las personalizaciones de CLR

Figura 2: algunas de las interfaces compatibles con las personalizaciones de CLR

He utilizado recuadros rojos para resaltar los dos gestores que se tratarán en esta entrada de blog: IHostMemoryManager  y IHostAssemblyManager . Pero primero, implementaremos nuestra interfazIHostControl  .

Implementación de IHostControl

Escribí mi prueba de concepto inicial para las personalizaciones CLR en C++, pero finalmente decidí reimplementarlo todo en C puro, que es lo que veremos en esta publicación. Prefiero los implantes escritos en C para evitar la sobrecarga de C++, por lo que quería que este arnés de ejecución de ensamblaje también estuviera en C. Implementar interfaces COM en C es una tarea enorme, pero espero que la siguiente información lo haga más fácil en el futuro. A continuación se muestra cómo debe definir la interfaz IHostControl , que he denominado “MyHostControl”.

Archivo de encabezado que implementa la interfaz IHostControl

Figura 3: archivo de encabezado que implementa la interfaz IHostControl

Para implementar nuestra interfaz COM, debemos tener los siguientes componentes (mostrados arriba en este orden):

  1. Un typedef para una estructura que contiene las funciones para nuestra interfaz. Las funciones QueryInterfaceAddRef  y Release   son repetitivas y estarán presentes en todas las interfaces COM. Las dos funciones de abajo, GetHostManager  y SetAppDomainManager , son específicas de esta interfaz.
  2. Un typedef para una estructura que define nuestra interfaz real, que tiene una tabla virtual (VTBL) y un recuento.
  3. Definiciones para las funciones específicas que implementaremos por separado. Les he puesto el prefijo “MyHostControl_”, ya que tendrá que definir QueryInterface /AddRef /Release  para cada interfaz COM.
  4. Una constante del VTBL que definimos anteriormente, pero con las funciones que hemos definido arriba.

La implementación de los métodos actuales es un poco más sencilla, como se muestra a continuación:

Implementación de los métodos QueryInterface, AddRef y Release

Figura 4: implementación de los métodos QueryInterface, AddRef y Release

Como he mencionado antes, los métodosQueryInterface /AddRef /Release  son repetitivos. Lo único que necesita cambiar para implementar una interfaz diferente es el valor “xIID_IHostControl” en el método QueryInterface  .

Implementación del método GetHostManager

Figura 5: implementación del método GetHostManager

Aquí no necesitamos implementar el método SetAppDomainManager  y simplemente podemos devolver E_NOTIMPL, siempre que no intentemos llamarlo más tarde. El método GetHostManager  , que es el núcleo de esta interfaz, es en realidad solo una serie de declaraciones “si” en las que comprobamos si el CLR nos pide un gestor que nos interese. En el código anterior, compruebo si el IID proporcionado es para la interfaz IHostMemoryManager  y, a continuación, creo un nuevo gestor de memoria que le apunta el argumento ppObject.

Iniciar el CLR

Ahora que hemos implementado nuestra interfaz IHostControl , podemos llamar SetHostControl y poner en marcha el CLR. A continuación hay un fragmento de código para hacer las tareas normales de alojamiento CLR (CLRCreateInstance , GetRuntime , GetInterface ), y luego llamar SetHostControl usando nuestra interfaz personalizada de control de host. Luego iniciamos el CLR.

Llamar a SetHostControl e iniciar el CLR

Figura 6: llamada a SetHostControl e inicio del CLR

Tomando el control de la memoria del CLR

Ahora que sabemos cómo implementar interfaces COM en C, implementar administradores específicos es más sencillo. La interfaz IHostMemoryManager nos permite tomar el control de la gestión de memoria del CLR. A continuación se muestra una lista de todas las funciones que necesitamos implementar para IHostMemoryManager .

Lista de métodos para la interfaz IHostMemoryManager

Figura 7: lista de métodos para la interfaz IHostMemoryManager

Probablemente note que algunos métodos permiten un comportamiento interesante, como los métodos virtuales* (VirtualAlloc, VirtualProtect, VirtualQuery, VirtualFree) que se utilizan para realizar la mayor parte de la gestión de la memoria en Windows. A continuación se muestra una implementación muy básica de estos métodos.

Implementación de VirtualAlloc, VirtualFree, VirtualQuery y VirtualProtect

Figura 8: implementación de VirtualAlloc, VirtualFree, VirtualQuery y VirtualProtect

Tomar el control sobre las asignaciones de memoria permite al operador ser tan creativo como desee. Por ejemplo, podría realizar las llamadas a la API de asignación mediante syscalls indirectas. También podría realizar un seguimiento de todas las asignaciones realizadas por el CLR y cifrarlas cuando su implante entre en modo de suspensión. Tenga en cuenta que el cifrado de las asignaciones de CLR no es muy estable. Además de los métodos virtuales*, también existe el método CreateMAlloc  , que devuelve una implementación de la interfaz IHostMalloc . Esta interfaz nos permite tomar el control de todas las asignaciones de Heap realizadas por el CLR.

Seguimiento y limpieza de artefactos de ensamblado

Anteriormente mencioné que intentar cifrar las asignaciones de memoria CLR pasa de ser “algo inestable” a “muy inestable”, dependiendo exactamente de cómo lo haga. Esto se debe a que si cifra o libera un fragmento de memoria que el CLR intenta consultar más adelante, el CLR lanzará un error y hará que su proceso se bloquee. Sin embargo, hay una excepción que he encontrado: las asignaciones hechas durante las cargas iniciales de ensamblado.

Cuando carga un ensamblador, ya sea en memoria o desde disco, el CLR asignará espacio y mapeará el ensamblador en memoria. Por lo que yo sé, no existía una buena forma públicamente conocida de identificar esta región de memoria y borrarla, aparte de buscar en la memoria del proceso patrones de bytes o asignaciones del tamaño esperado. Las personalizaciones de CLR proporcionan un mecanismo sencillo para realizar un seguimiento de estas asignaciones en forma del método AcquiredVirtualAddressSpace . Este método es una devolución de llamada de notificación que se activa cada vez que CLR carga un ensamblado en el proceso, y la devolución de llamada incluye la dirección y el tamaño de la asignación como argumentos. Según mis pruebas, esta llamada solo se activa cuando se carga un ensamblador en el proceso, lo que nos proporciona una buena forma de llevar un control de las asignaciones de carga del ensamblaje. Para mayor solidez, puede comprobar el tamaño o analizar la memoria para asegurarse de que es el ensamblado que espera. A continuación se muestra un ejemplo de implementación de este método. Dado que es solo una devolución de llamada de notificación, puede hacer lo que quiera y luego simplemente devolver S_OK.

Implementación del método AcquiredVirtualAddressSpace

Figura 9: implementación del método AcquiredVirtualAddressSpace

A diferencia de las otras asignaciones realizadas por CLR, no he tenido ningún problema para cifrar o borrar esta región de memoria después de que el ensamblado haya terminado de ejecutarse. Puede tener problemas si intenta ejecutar el mismo ensamblado en el mismo dominio de aplicación de nuevo, ya que el CLR podría intentar utilizar el ensamblado almacenado en caché que ahora no es válido. La mayoría de las implementaciones de ejecución del ensamblado crearán un nuevo dominio de aplicación y lo destruirán después de la ejecución, así que asegúrese de probar su implementación.

Esta funcionalidad de notificación también tiene una aplicación defensiva menor. Por lo general, Defensive Products utiliza el Event Tracing for Windows (ETW) para realizar un seguimiento de las cargas de ensamblado en la CLR, pero esto proporciona otra forma de recibir una notificación si se ha cargado un ensamblado en el proceso. Como la dirección y el tamaño de la memoria están incluidos, sería trivial que un producto defensivo realizara un análisis de memoria en esa región.

Gestión de cargas de ensamblado

Los otros gestores que examinaremos son IHostAssemblyManager y IHostAssemblyStore . IHostAssemblyManager es responsable de dos cosas: dar al CLR una lista de ensamblados que debe manejar la carga (en lugar de nosotros, como host del CLR) y devolver una interfaz IHostAssemblyStore con el CLR. IHostAssemblyStore tiene dos métodos: ProvideAssembly y ProvideModule .

ProvideAssembly se llamará a cada vez que se pida al CLR que cargue un ensamblado que no esté en la lista de ensamblados que el CLR es responsable de cargar (devuelto por IHostAssemblyManager ). El CLR llama a ProvideAssembly y le pasa la cadena de identidad de un ensamblaje, y ProvideAssembly se encarga de devolver los bytes del ensamblaje. Probablemente haya visto una cadena de identidad antes, se parece a: “Seatbelt, Version=0.0.0.0, PublicKeyToken=null, Culture=neutral ”.

Una vez que ProvideAssembly ha resuelto el ensamblado, el contenido del ensamblado se devuelve estableciendo un puntero que se proporciona como argumento. He destacado el argumento pertinente, ppStmAssemblyImage , en la captura de pantalla de abajo.

Argumentos a favor del método ProvideAssembly de la interfaz IHostAssemblyStore

Figura 10: argumentos a favor del método ProvideAssembly de la interfaz IHostAssemblyStore

El ensamblado se devuelve estableciendo el puntero en la dirección de un IStream en memoria. Normalmente, el CLR intentaría localizar el ensamblado siguiendo su orden de búsqueda de directorios en el disco, de forma similar a cargar una DLL en un proceso normal de Windows. Pero como podemos ofrecer nuestra propia implementación, podemos aceptar una solicitud de ensamblado que normalmente se cargaría desde el disco y, en su lugar, proporcionar un ensamblado que tengamos en memoria. Los bytes del ensamblado sí necesitan estar en un IStream, lo que puede lograrse utilizando la funció SHCreateMemStream que toma una matriz de bytes y devuelve un IStream.

Quizás te preguntes por qué esto es importante si es simplemente otra forma de cargar ensamblados en la memoria. ¿Qué ocurre con la Anti-Malware Scan Interface (AMSI)?

Eludir AMSI

AMSI es responsable de escanear cualquier ensamblado que se cargue de manera reflexiva en busca de contenido malicioso. Windows Defender utiliza AMSI, y AMSI también permite que otros EDR se conecten y analicen el contenido de los ensamblados cargados en la memoria. Algunos tienden a despreciar AMSI, ya que se puede eludir, pero creo que para algo que se instala en Windows de forma predeterminada, AMSI es una característica de seguridad bastante eficaz. Como mínimo, detectará una gran cantidad de herramientas maliciosas .NET (como Seatbelt) que se ejecutan en memoria. Hay un amplio historial de jugar al gato y al ratón eludiendo AMSI entre los equipos rojos, pero muchas de las elusiones de AMSI se basan en parchear los bytes de las funciones clave (como AMSIScanBuffer) para que no se ejecuten o devuelvan un valor “bueno”. Las elusiones de AMSI tradicionales son engorrosos ya que dejan bytes Copy-on-Write en la sección .text de la memoria de AMSI.dll, que son un indicio inequívoco para cualquier defensor que observe un proceso sospechoso. Las elusiones de AMSI más sofisticadas, como los ganchos de punto de interrupción de hardware, también tienen otros IOC asociados, como inspeccionar el contexto del hilo para buscar el uso del registro de depuración.

Implementando nuestra propia versión del método ProvideAssembly , podemos eludir completamente el AMSI. Tradicionalmente, el métodoLoad_3 se utiliza para cargar conjuntos desde un array de bytes y Load_3 es instrumentado por AMSI. Pero, ¿sabía que hay otras funciones Load_* que rara vez se utilizan?

La familia de métodos Load

Figura 11: la familia de métodos Load

Load_2  toma una cadena de identidad de ensamblaje como argumento en lugar de un array de bytes como Load_3 . Normalmente, esto significaría que el ensamblado tiene que estar en algún lugar del disco donde el CLR pueda encontrarlo, pero sabemos que cuando se le pide al CLR que cargue un ensamblado por identidad, le pedirá a nuestra implementación  ProvideAssembly que proporcione ese ensamblado. También sabemos que podemos devolver un array de bytes en memoria (en un IStream) desde ProvideAssembly . Esto significa que podemos llamar a Load_2  y proporcionar al CLR un ensamblado en memoria que cargará.

Ahora la parte importante: porque estamos llamando a Load_2 , el CLR cree que estamos cargando nuestro ensamblador desde disco y AMSI no escanea nuestros bytes de ensamblador. De hecho, AMSI ni siquiera se carga en el proceso.

A continuación se muestra un ejemplo de ejecución de Seatbelt a través de una llamada a  Load_2  sin que AMSI se cargue en el proceso.

Ejecutar el cinturón de seguridad y eludir AMSI

Figura 12: ejecución del cinturón de seguridad y elusiones de AMSI

Una lista de módulos de proceso sin AMSI.dll cargado

Figura 13: una lista de módulos de proceso sin AMSI.dll cargada

Según un artículo de CLR Inside Out publicado en la edición de agosto de 2006 de la revista MSDN, Microsoft mismo ha utilizado una técnica similar para que SQL Server 2005 cargue ensamblados .NET desde la base de datos en lugar de desde el disco. La capacidad de almacenar y ejecutar ensamblados .NET desde una SQL database es una de mis técnicas de movimiento lateral favoritas y se puede hacer fácilmente usando SQLRecon, otra herramienta X-Force Red escrita por Sanjiv Kawa.

Prueba de concepto y posterior operacionalización

Estoy publicando una prueba de concepto de esta técnica que puede encontrar en GitHub aquí. Esta prueba de concepto muestra cómo implementar las interfaces COM IHostControl , IHostMemoryManager , IHostMalloc , IHostAssemblyStore  y IHostAssemblyManager . Llama a SetHostControl e inicia el CLR utilizando la interfaz ICLRRuntimeHost .

La implementación del administrador de memoria simplemente llama a las API de Windows correctas (por ejemplo, VirtualAlloc), pero incluye un ejemplo de seguimiento de todas las asignaciones de memoria realizadas por el CLR. También incluye un ejemplo de cómo borrar los artefactos de carga ensambladora proporcionados por la devolución de llamada AcquiredVirtualAddressSpace mencionada anteriormente.

También hay una prueba de concepto completa para el bypass AMSI. Hay una advertencia con esta omisión si intenta implementarla en sus propias herramientas: la identidad del ensamblado que intente cargar utilizando el método Load_2 debe coincidir con la identidad del ensamblado que finalmente devuelva al CLR. Por ejemplo, si llama a Load_2 con un argumento de “Seatbelt, Version=0.0.0.0, PublicKeyToken=null, Culture=neutral”, entonces el ensamblado que finalmente intenta ejecutar también debe tener esta identidad. No puede intentar cargar mscorlib, sino devolver Seatbelt, ya que el CLR lo comprobará y arrojará un error. Tenga en cuenta qué nombres de ensamblado está intentando cargar, pero si todavía está intentando cargar reflexivamente un ensamblado llamado Seatbelt en cualquier año en el que esté leyendo esto, le sugiero que cierre esta entrada de blog y realice una actividad más satisfactoria.

En la prueba de concepto, utilizo el metodo GetBindingIdentityFromStream de la interfaz ICLRAssemblyIdentityManager para obtener la cadena de identidad del conjunto que se va a ejecutar. También puede separar esto del implante y obtener la identidad del conjunto en el cliente o el servidor del equipo, y luego pasar la cadena de identidad como argumento a su implante.

Consideraciones defensivas

Aunque se trata de una novedosa elusión de AMSI, en última instancia es solo eso: una elusión de AMSI. Los productos defensivos deberían utilizar estrategias de defensa profundas y no depender simplemente de AMSI para detectar ensamblados maliciosos. Cualquier ensamblado cargado usando esta técnica también generará los mismos eventos de Event Tracing for Windows (ETW) que cualquier otro ensamblado en memoria. Los ensamblados maliciosos se pueden detectar mediante el análisis de memoria, como hemos visto en varias plataformas de EDR (detección y respuesta de endpoints) más avanzadas. Muchas herramientas post-explotación también tienen sus propios IOCs únicos.

Como se ha mencionado anteriormente, también hay algunas aplicaciones defensivas de esta investigación. La devolución de llamada AcquiredVirtualAddressSpace proporciona otro método para recibir notificaciones cuando se cargan ensamblados en el proceso. Si un defensor implementara la interfaz IHostAssemblyStore , se insertaría en la cadena de carga del ensamblado y tendría la capacidad de bloquear por completo las cargas del ensamblado o modificar los bytes de un ensamblado antes de que se cargue en el proceso. Voy a arriesgarme y decir que creo que es muy probable que haya avances futuros en este ámbito.

¿Por qué publicar esto ahora?

Me gustaría referirme a nuestra línea de tiempo de esta investigación y por qué la publicamos ahora. Realicé toda esta investigación en junio de 2023 y la hemos mantenido privada desde entonces, aunque se ha presentado en varias conferencias CFP. En el momento en que se realizó esta investigación, busqué en motores de búsqueda algo similar, inicialmente buscando material de referencia y luego buscando confirmar si fui el primero en identificar este comportamiento. Desde entonces, he realizado búsquedas periódicas para ver si alguien había publicado algún trabajo similar. A principios de enero de 2025 encontré esta pieza: Using CLR Hosting to Evade AMSI, de Marcos González Hermida en la revista “NTT Data”.

Este artículo reveló este mismo desvío y, según la revista, se publicó inicialmente en junio de 2024 (el enlace complementario anterior es de julio de 2024). Es un texto excelentemente escrito y le recomendaría que lo lea. Las únicas observaciones que haría son que el autor concluye que los ensamblados deben estar firmados para poder ejecutarse y que, en su prueba de concepto, utilizan el método GetType_2 para obtener manualmente el método Main del ensamblado que cargan (Rubeus, concretamente). No he tenido problemas para cargar ensamblados sin firmar con esta técnica, y puede usar el mismo método Entrypoint para obtener el punto de entrada del ensamblado cargado que se utiliza en muchas implementaciones de execute-assembly, sin tener que conocer los nombres del espacio de nombres o de las clases.

Con la publicación del artículo de Marcos y la prueba de concepto, esta elusión de AMSI ahora podría considerarse pública, por lo que decidimos que era hora de publicar nuestra investigación junto con una prueba de concepto de lo que descubrimos.

Conclusión

En esta publicación, analizamos cómo pueds usar las personalizaciones de CLR para mejorar su OPSEC mientras ejecuta herramientas .NET en memoria. Además, demostramos una elusión completa de AMSI utilizando estas características CLR menos conocidas. El uso de las herramientas de.NET seguirá siendo eficaz para los profesionales de la ofensiva y los actores de amenazas. Por este motivo, es fundamental que los defensores comprendan cómo funciona el CLR y construyan estrategias de defensa en profundidad para detectar herramientas posteriores a la explotación.

Agradecimientos

¡Gracias Brett y Valentina por revisar esta investigación!

  • Brett Hawkins (@h4wkst3r)
  • Valentina Palmiotti (@chompie1337)

Trabajos relacionados

Dealing with Failure: Failure Escalation Policy in CLR Hosts – Este es el único ejemplo real que pude encontrar de espionaje ofensivo utilizando personalizaciones de CLR cuando inicialmente estaba haciendo esta investigación.

Hosted Pumpkin : un repositorio de GitHub que contiene una prueba de concepto para implementar varias personalizaciones de CLR.

Shellcode: Loading .NET Assemblies From Memory – Donut fue de gran ayuda para resolver todas las estructuras de datos y definiciones relevantes en C.

Customizing the Microsoft .NET Framework Common Language Runtime por Steven Pratschner – Este es el texto definitivo sobre las personalizaciones de CLR. Simplemente una lectura obligada si tienes algún interés en esta área.