Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
MemFiles — Un kit de herramientas para CobaltStrike que escribe los archivos generados por Beacon en memoria en lugar de en disco. | Kitploit
Herramientas/GitHubGitHub/octoberfest7/memfiles
Exfiltración de DatosPost-ExplotaciónComando y ControlRed Teaming
GitHuboctoberfest7/memfiles

MemFiles

Un kit de herramientas para CobaltStrike que escribe los archivos generados por Beacon en memoria en lugar de en disco.

Ver Repositorio
47762hace 2 añosRevisado por Kitploit

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

MemFiles

AVISO LEGAL:

Este proyecto es complejo; no entender cómo funciona y no probarlo adecuadamente puede provocar que los Beacons se bloqueen y que pierdas el acceso.

Recomiendo encarecidamente leer toda la documentación hasta la sección "Detalles técnicos, consideraciones de diseño y comentarios".

Introducción

MemFiles es un conjunto de herramientas para CobaltStrike que permite a los operadores escribir en memoria los archivos producidos por el proceso Beacon, en lugar de escribirlos en el disco del sistema objetivo. Se ha probado con éxito en Windows 7, 10 y 11; las versiones de servidor correspondientes deberían funcionar sin problemas. MemFiles está restringido a Beacons x64.

Lo consigue enganchando (hookeando) varias NtAPI diferentes dentro de NTDLL.dll y redirigiendo las llamadas a esas API a funciones que se han inyectado en el espacio de memoria del proceso Beacon.

MemFiles asume que hay una copia limpia/sin enganchar de NTDLL en el proceso Beacon. No se ofrecen garantías sobre la viabilidad de MemFiles en un proceso Beacon donde los hooks de EDR sigan presentes. ¡Repara/actualiza NTDLL antes de usar MemFiles!

Dentro del conjunto de herramientas MemFiles se define un directorio "especial" e inexistente; cualquier archivo que se escriba en este directorio especial será capturado por MemFiles y escrito en memoria, desde donde podrá descargarse posteriormente al Teamserver.

MemFiles es compatible con la mayoría (no todos) de las herramientas que se ejecutan dentro del proceso Beacon y a las que se les puede indicar que escriban su salida en un directorio específico. NO requiere privilegios elevados para funcionar.

Esto incluye:
-BOFs
-Ensamblados .NET ejecutados en línea con algo como inline-executeAssembly
-PEs ejecutados en línea con algo como Inline-Execute-PE

Todos ellos son compatibles porque se ejecutan dentro del proceso Beacon, donde las NtAPI relevantes han sido enganchadas.

MemFiles NO funciona con cosas como:
-execute-assembly
-shell
-run

Ninguno de ellos es compatible porque todos generan otros procesos cuyas NtAPI no han sido enganchadas.

MemFiles se ha probado con éxito con herramientas como Rubeus, SharpHound, Procdump y Powershell cuando se ejecutan dentro del proceso Beacon.

Configuración

Clona el repositorio y, opcionalmente, modifica la variable hookdir definida en la línea 56 tanto en /PIC/Source/NtCreateFile.c como en /PIC/Source/NtOpenFile.c. Esta variable es el directorio "especial" que le indica a MemFiles que debe interceptar el archivo que se está creando. La variable hookdir está configurada como "redteam" por defecto. ¡Asegúrate de que esta variable no sea un directorio real en el sistema objetivo y de que sea la misma en ambos archivos!

image

Ejecuta 'make all' para compilar tanto los BOF necesarios como las funciones PIC.

Carga MemFiles.cna en el Cliente de CobaltStrike. Asegúrate de que el directorio desde el que se ejecuta CobaltStrike sea escribible por tu usuario; MemFiles crea allí un archivo de texto (memfiles.txt) para garantizar la disponibilidad de los datos necesarios para que MemFiles funcione.

MemFiles puede configurarse para instalarse en cada nuevo Beacon que se comunique con el Teamserver; esto se consigue mediante el elemento de menú MemFiles->Config. Por defecto, MemFiles NO se autoinstala en Beacons nuevos. Ten en cuenta que esta es una configuración global; si dos Clientes están conectados al Teamserver y ambos tienen MemFiles.cna cargado, si el Cliente A activa la opción "Install on beacon initial", el cambio también tendrá efecto para el Cliente B.

image

Comandos

MemFiles se compone de 4 comandos orientados al objetivo que ejecutan BOF y 1 comando interno que manipula la estructura de datos del proyecto.

Orientados al objetivo:

  1. meminit
  2. memlist
  3. memfetch
  4. memclean

Estructura de datos interna:

  1. memtable

meminit

meminit se encarga de instalar MemFiles en el proceso Beacon.

La lista de NtAPI enganchadas por MemFiles es la siguiente:

  1. NtCreateFile
  2. NtWriteFile
  3. NtClose
  4. NtQueryVolumeInformationFile
  5. NtQueryInformationFile
  6. NtSetInformationFile
  7. NtOpenFile
  8. NtReadFile
  9. NtFlushBuffersFile

meminit realiza las siguientes acciones principales:

  1. Envía al Beacon una función de reemplazo independiente de la posición para cada NtAPI enganchada
  2. Crea una estructura en la memoria del Beacon para almacenar varios valores necesarios para MemFiles a lo largo de su ciclo de vida
  3. Parchea la dirección de esta estructura en cada una de las funciones de reemplazo PIC
  4. Asigna memoria e inyecta cada función de reemplazo PIC en el espacio de memoria del proceso Beacon
  5. Crea un trampolín para cada NtAPI enganchada
  6. Engancha cada NtAPI listada sobrescribiendo algunos/todos los bytes, redirigiendo la ejecución a la función de reemplazo PIC.

memlist

memlist se utiliza para mostrar todos los archivos almacenados actualmente en memoria por MemFiles para un Beacon determinado.
image
Se muestran varios campos; los más relevantes y de interés para el usuario son el nombre del archivo y la longitud de los datos almacenados.

memfetch

memfetch se utiliza para recuperar realmente los archivos almacenados en memoria por MemFiles para un Beacon determinado.

Por defecto, memfetch recupera todos y cada uno de los archivos almacenados por MemFiles cuyo "handle" haya sido cerrado. Esta decisión de diseño se tomó para evitar problemas relacionados con intentar descargar un archivo que un programa/aplicación no ha terminado de escribir.

Esto significa que si un programa/aplicación no cierra el handle que abrió para el archivo, memfetch no descargará dicho archivo.
Esto se puede mitigar usando el argumento "force" con memfetch, es decir, 'memfetch force' para recuperar todos los archivos de la memoria independientemente del estado de su handle.

Los archivos que memfetch recupera de la memoria se envían de vuelta al Teamserver como una descarga y pueden sincronizarse desde el Teamserver al Cliente a través de la pestaña Downloads en CobaltStrike.

Una vez que el Teamserver ha descargado un archivo, este se borra de la memoria del proceso Beacon y su entrada, mostrada mediante memlist, se elimina.

memclean

memclean se encarga de limpiar y eliminar MemFiles de un proceso Beacon.

El caso de uso estándar de MemFiles implica instalarlo y dejarlo instalado durante toda la vida del Beacon; sin embargo, si se quiere usar MemFiles junto con una herramienta para capturar y recuperar la salida de archivos, y luego desinstalar MemFiles para que sus artefactos no queden en memoria, se puede usar memclean para revertir el proceso Beacon a su estado original antes de ejecutar meminit.

Esto implica:

  1. Desenganchar cada NtAPI enganchada
  2. Poner a cero y liberar cada trampolín creado
  3. Poner a cero y liberar cada función de reemplazo PIC inyectada
  4. Poner a cero y liberar la estructura MemFiles

Ten en cuenta que antes de realizar estas acciones, memclean descargará forzosamente cualquier archivo almacenado en memoria por MemFiles. Si se pretende usar MemFiles con una sola herramienta y luego eliminarlo, se puede omitir memfetch y usar memclean para recuperar los archivos Y eliminar MemFiles del proceso Beacon de una sola vez.

memtable

memtable se utiliza para mostrar y rastrear información sobre los Beacons en los que MemFiles está actualmente instalado. También muestra información de configuración global.

Cada Cliente de CobaltStrike tiene su propio memtable; MemFiles se esfuerza al máximo para garantizar la sincronización de sus datos entre todos los Clientes de CobaltStrike conectados, de modo que MemFiles pueda ser utilizado por todos los Operadores en todos los Beacons. Para más información, consulta "Consideraciones de diseño y comentarios".

image

Uso

Inicializa MemFiles en un Beacon usando el comando meminit. Esto se puede configurar para que ocurra automáticamente activando la opción en el menú MemFiles->Config.

image

Con MemFiles inicializado, ¡ya puedes usar tus herramientas favoritas para escribir archivos en memoria! La forma de hacerlo dependerá de la herramienta concreta; algunas permiten especificar un directorio para volcar varios archivos, mientras que otras permiten especificar una ruta absoluta para un único archivo creado por la herramienta. A continuación se muestran algunos ejemplos:

SharpHound:

Aquí especificamos que SharpHound debe enviar todos los archivos producidos al directorio c:\redteam\ (nuestro directorio especial de MemFiles) y que no debe comprimir los archivos; MemFiles no admite que los programas lean archivos desde la memoria, solo que los escriban, por lo que la funcionalidad de compresión en SharpHound no funciona.

image

Rubeus:

El comando "dump" se usa con Rubeus y le indicamos que envíe toda la salida de consola a un archivo (ubicado en nuestro directorio especial)

image

Powershell:

En este ejemplo, se utiliza Inline-Execute-PE para cargar powershell.exe en el proceso Beacon y ejecutar 'Get-ADUser' para obtener una lista de usuarios del dominio. Usando una tubería (pipe) y 'out-file', los datos se pueden escribir en la memoria y luego recuperarlos.

image

Cuando quieras recuperar tus archivos, ejecuta memfetch:

image

Cuando hayas terminado con MemFiles y/o no quieras dejarlo instalado en un proceso Beacon, ejecuta memclean:

image

Ten en cuenta que en el ejemplo anterior había un archivo que aún no se había descargado; memclean descarga este archivo y lo borra de la memoria antes de desinstalar MemFiles.

Consulta el estado y la configuración de MemFiles usando memtable. Durante operaciones largas, elimina de memtable las entradas de Beacons muertos/antiguos para evitar saturación.

image

Capacidades y limitaciones

Como se enfatizó en la Introducción, MemFiles requiere una copia limpia de NTDLL en el proceso Beacon para funcionar. Esto es necesario porque lee los bytes originales de la NtFunction y copia ciertos de ellos al trampolín, que luego se usa para completar llamadas normales a la NtFunction con las que MemFiles no debe interferir. Este tema se trata con más detalle en la sección "Detalles técnicos, consideraciones de diseño y comentarios".

MemFiles realiza una asignación inicial de 1048576 bytes para cada archivo; a medida que se escriben datos en la memoria, puede ampliar esta asignación y lo hará cuando sea necesario para contener archivos más grandes.

El nombre de archivo almacenado en la estructura MemFiles se extrae de un argumento que se pasa a la función de reemplazo NtCreateFile. MemFiles hace esto de una manera bastante simple: localiza el directorio "especial" en el argumento de ruta del archivo, avanza hasta el final del mismo y luego incrementa el puntero en 1 para tener en cuenta el carácter '\' que separa el directorio "especial" del nombre de archivo. Por ejemplo, en la ruta 'C:\users\tom\redteam\myfile.txt', MemFiles localiza 'redteam', tiene en cuenta la barra invertida y selecciona 'myfile.txt' como nombre de archivo.

A MemFiles no le importan los directorios anteriores en la ruta del archivo; 'C:\redteam\myfile.txt' y 'c:\users\tom\appdata\local\redteam\myfile.txt' son rutas igualmente válidas en lo que a MemFiles respecta.

Dado cómo MemFiles extrae los nombres de archivo de las rutas, cabe señalar que MemFiles no admite la creación de subdirectorios; esto significa que MemFiles no funcionará correctamente con herramientas que, por ejemplo, intenten crear c:\redteam\mynewdir\file1.txt, c:\redteam\mynewdir\file2.txt, c:\redteam\mysecondir\file3.txt, etc.

Las NtAPI enganchadas por MemFiles han sido identificadas como las que varios programas usan para operaciones de E/S. Como se mencionó anteriormente, las herramientas/capacidades probadas con éxito con MemFiles/este conjunto de NtAPI enganchadas son: SharpHound, Rubeus, Powershell, Procdump, BOF, programas C genéricos que realizan operaciones de escritura de archivos y el comando bupload_raw de CobaltStrike (que permite al Operador especificar la ubicación remota del archivo). Seguramente hay otras herramientas que funcionarán con MemFiles sin configuración adicional; otras serán incompatibles.

A primera vista, el proceso de creación de archivos en Windows parece sencillo: NtCreateFile->NtWriteFile->NtClose. Rápidamente, al profundizar en este proyecto, descubrí que hay una serie de otras API implicadas y, para mayor complicación, las otras API implicadas difieren entre programas. Algunos programas, como parte de su proceso de E/S, llaman a la API Win32 SetFilePointerEx, que a su vez llama a la NtAPI NtSetInformationFile. Otros, como los programas .NET incluido SharpHound, terminan llamando a NtFlushBuffersFile.

La falta de una única cadena común de llamadas API para crear y escribir archivos abre espacio para incompatibilidades según la herramienta con la que se utilice MemFiles. Un programa podría llamar a otra NtAPI que MemFiles no ha enganchado, pasando el handle falso creado por MemFiles en la función de reemplazo NtCreateFile y provocando un error "Invalid Handle" que detiene la ejecución. Otros programas realizan acciones más complejas que MemFiles no puede suplantar/reemplazar adecuadamente. Una de estas incompatibilidades ya identificadas es ADExplorer.exe.

ADExplorer.exe es un binario firmado por Microsoft utilizado para la enumeración de Active Directory. Durante la ejecución, ADExplorer escribe datos en el archivo de salida especificado y luego, más tarde, vuelve a consultarlo antes de escribir finalmente la salida final en el archivo al final de la ejecución. Debido a que utiliza el archivo de salida como una especie de caché, tiene que poder leer los datos que ya ha escrito en el archivo, y es muy poco probable que lo haga de una manera simple y predecible.

MemFiles actualmente no admite que los programas o aplicaciones lean archivos en memoria, pero esto debería ser posible desarrollando más a fondo la función personalizada NtReadFile y añadiendo algunas variables adicionales/seguimiento de datos a la estructura MemFiles.

ADExplorer presenta otro desafío en cuanto al tamaño de los archivos que produce. En entornos empresariales grandes, el archivo de salida puede superar 1GB; aunque MemFiles debería poder manejarlo programáticamente, ciertamente no está pensado para tales casos de uso.

Herramientas incompatibles

Sin duda, la comunidad descubrirá herramientas con las que MemFiles no funciona correctamente; te animo a abrir un issue detallando el programa/herramienta incompatible y las circunstancias en las que lo ejecutaste, es decir, si es un BOF, mediante inline-executeAssembly, Inline-Execute-PE, etc., para ver si puedo ampliar MemFiles y conseguir que funcione.

IOC y AV/EDR

Los IOC asociados con MemFiles incluyen, entre otros:

Asignación de memoria mediante VirtualAlloc
Escritura de datos mediante WriteProcessMemory
Cambio de las protecciones de memoria en la memoria asignada entre RW y RX
Sobrescritura de memoria dentro de NTDLL.dll

AV/EDR

MemFiles no fue desarrollado ni probado contra un EDR adecuado; Microsoft Defender fue lo que estaba disponible. Dicho esto, me atrevería a decir que es más probable que se alerte sobre la herramienta/programa que Beacon está ejecutando para producir un archivo que sobre la captura o el almacenamiento en memoria de ese archivo por parte de MemFiles. La sobrescritura de memoria en NTDLL/el enganche de las NtAPI me parece algo que algunos productos podrían cuestionar, pero no tengo evidencia que lo corrobore. Para las llamadas a NtFunctions enganchadas que no conciernen a archivos que son/deberían ser capturados por MemFiles, la syscall se emite igualmente desde el espacio de direcciones de NTDLL.dll, ya que los productos de seguridad detectan y alertan sobre syscalls realizadas desde fuera de esta área.

Cabe señalar que los archivos almacenados en memoria por MemFiles NO están codificados ni cifrados; esta característica podría añadirse si se identifica un caso de uso/instancia real donde AV/EDR esté alertando sobre un archivo producido en memoria.

Detalles técnicos, consideraciones de diseño y comentarios

Me introduje por primera vez al concepto de un sistema de archivos en memoria gracias a una charla de conferencia hace varios meses en la Tradecraftcon de KFiveFour, donde un ponente (@DexterGerig) demostró un POC que creaba un sistema de archivos en memoria usando un modelo cliente-servidor. La mitad de la funcionalidad que imaginé para un proyecto así estaba cubierta por mi último gran lanzamiento, Inline-Execute-PE. La otra mitad, la idea de poder capturar archivos producidos por las herramientas y almacenarlos en memoria en lugar de en disco, no fue cumplida por ese proyecto y siguió siendo una capacidad muy deseable por razones obvias.

MemFiles fue un proyecto increíblemente desafiante para mí, ya que antes de este proyecto había pasado muy poco tiempo con un depurador, no tenía conocimientos de ensamblador y no entendía el hooking de API. Encontré varios obstáculos de 10 a 20 horas de duración durante este proyecto que, gracias a la persistencia, pude superar. Aunque probablemente no fue la forma más eficiente de hacerlo, gané mucha familiaridad con los depuradores y una mayor comprensión de cómo funcionan los ordenadores internamente en lo que respecta al ensamblador, los registros, la pila y las convenciones de llamada.

A continuación se presenta un análisis técnico en profundidad de algunos de los detalles técnicos y consideraciones de diseño más importantes que han dado forma a MemFiles.

Archivos en Windows y cómo funciona MemFiles

La creación de archivos en Windows comienza con NtCreateFile, al que se le proporciona la ruta del archivo deseado y, a cambio, Windows crea un archivo en esa ubicación y proporciona un handle al mismo. El handle devuelto se utiliza en todas las llamadas posteriores que involucran al archivo, por ejemplo, a NtWriteFile y NtClose.

Al pensar en cómo separar las llamadas a todas estas API entre las que queremos interceptar y manipular y las que queremos dejar en paz, decidí buscar una palabra clave en la llamada a NtCreateFile. Esto se logró especificando un directorio único e inexistente como parte de la ruta del archivo en la llamada a NtCreateFile. Cuando nuestro hook redirige la ejecución a la función de reemplazo NtCreateFile, la ruta del archivo pasada como argumento a NtCreateFile se examina en busca de esa "palabra clave" única; si la encuentra, MemFiles sabe que esta llamada a NtCreateFile se refiere a un archivo que debe colocarse en memoria en lugar de en disco. Cuando esto ocurre, MemFiles inicializa varias variables y asigna una memoria inicial de 1MB para uso del archivo, pero lo más importante es que asocia un handle falso al nombre de archivo especificado en la estructura MemFiles y devuelve este handle falso al llamador.

Para todas las demás NtAPI enganchadas por MemFiles, las correspondientes funciones de reemplazo NtFunction examinan el handle pasado como argumento y comprueban si existe en la estructura MemFiles; si el handle existe (los handles falsos producidos por MemFiles son suficientemente falsos como para que nunca se crucen con uno real) en la estructura MemFiles, MemFiles identifica esta llamada como una que concierne a un archivo en memoria y actúa en consecuencia.### Teoría del hooking y funciones de reemplazo Para poder escribir en memoria archivos que estaban destinados al disco, MemFiles necesita interceptar llamadas a ciertas API que los programas realizan cuando intentan crear un archivo. El API Hooking existe desde hace mucho tiempo y es utilizado activamente por muchos productos EDR como parte central de su funcionalidad; las llamadas a ciertas API se redirigen al espacio de direcciones del EDR, donde se analiza la llamada a la API y las variables que se le pasan. Si el EDR determina que la llamada es maliciosa, por ejemplo, parte de una herramienta de ataque o de una kill chain, evitará que la llamada se complete y generará una alerta. Si el EDR decide que la llamada es benigna, parcheará la ejecución de vuelta al punto desde el que se redirigió y permitirá que la llamada a la API se complete como se pretendía originalmente. Una analogía simplista sería decir que envías una carta a un amigo, pero antes de que tu amigo la reciba, un tercero la abre, la lee y decide si hay algo ilegal en la carta; en ese caso, tu amigo nunca recibe la carta y se alerta a la policía.

MemFiles sigue la misma teoría, sin las alertas (ni la participación policial teórica). El hooking de API se implementa normalmente en el nivel más bajo posible en userland; las NtFunctions dentro de NTDLL.dll. Echemos un vistazo a NtCreateFile, antes de que se haya realizado ningún hooking:

image

Todas las NtFunctions son idénticas, con la excepción del número de syscall, que en este ejemplo es 55. El número de syscall cambia entre NtFunctions, y también cabe señalar que este número puede cambiar entre versiones de Windows; el número de syscall para NtCreateFile en este SO (Windows 11) es 55, sin embargo podría ser diferente en Windows 10 (y seguramente en Windows 7).

Cabe destacar las instrucciones TEST y JNE. Estas existen para determinar si la NtFunction debe usar la instrucción syscall normal o la instrucción heredada INT 2E. Citaré el post de klezvirus SysWhispers is dead, long live SysWhispers!:

Ahora viene la parte interesante: la función comprueba si SharedUserData[0x308] (BYTE PTR DS:[7FFE0308]) está establecido en 1. SharedUserData es un símbolo que hace referencia a la estructura del modo kernel KUSER_SHARED_DATA.

La estructura KUSER_SHARED_DATA define un espacio de memoria fijo (o predefinido) que se utiliza para compartir información con el software en modo usuario. Esto, por supuesto, se hizo para que cierta información global del sistema esté lista para ser consumida por el código en user-land sin la sobrecarga de tener que cambiar cada vez entre la ejecución en modo usuario y en modo kernel.

El valor en el índice 0x308 representa la instrucción syscall, soportada en todas las versiones de Windows desde la 1511. Como imaginarás, en todas las versiones de Windows anteriores a la 1511, la forma estándar de ejecutar un syscall era mediante la interrupción int 2Eh.

...

Si te preguntas por qué este int 2Eh sigue ahí, incluso cuando Windows está ahora muy por encima de la versión 1511, es porque esta instrucción todavía se usa. De hecho, cuando HVCI (Hypervisor-protected Code Integrity) está habilitado, SharedUserData[0x308] se establece a 0, y se usa int 2Eh en lugar de la instrucción syscall. Esto se hace principalmente por razones de rendimiento, debido a cómo se realiza el cambio de Ring3 a Ring0 usando una u otra instrucción.

Pedí más aclaraciones sobre este tema en Twitter, a lo que @yarden_shafir respondió lo siguiente:

image

En resumen, cada instrucción de la NtFunction podría ser necesaria en algún momento (quizás con la excepción del NOP multibyte que se encuentra al final y que parece inalcanzable), y si sobrescribimos instrucciones en la NtFunction, debemos asegurarnos de guardarlas y ejecutarlas en algún momento antes de realizar el syscall final (o INT 2E, según sea el caso).

Cabe señalar que, antes de realizar el syscall, el número de syscall se mueve a RAX (mostrado como EAX en la captura de pantalla). Como no vemos que RAX se apile en la pila antes de esto, asumí (quizás de forma ingenua) que el valor contenido en RAX antes de que se mueva allí el número de syscall no es importante ni se requiere más adelante después de que se emita el syscall. Esta es una buena noticia, ya que significa que podemos usar el registro RAX libremente siempre que nos aseguremos de que contenga el número de syscall antes de emitirlo.

Para redirigir la ejecución a nuestro código personalizado/función NtFunction de reemplazo, sobrescribiremos parte de la NtAPI original, moviendo la dirección de nuestra NtFunction de reemplazo a RAX y usando luego una instrucción JMP para ir a ese código:

image

Se requieren 12 bytes para las instrucciones MOV y JMP; debido a que estamos alterando otras instrucciones al sobrescribir los primeros 12 bytes de la NtAPI, esas instrucciones han sido reemplazadas por NOP's para mantener el espaciado y la alineación adecuados de la NtAPI.

Cuando el programa llama ahora a NtCreateFile, saltará la ejecución a nuestra NtFunction de reemplazo.

Código personalizado y NtFunctions de reemplazo

MemFiles se desvía de la forma en que los EDR realizan el hooking en cuanto a dónde residen las funciones de reemplazo a las que se redirigen las API hookeadas. Muchos EDR cargan su propio DLL en un proceso. Las API hookeadas se redirigen al espacio de direcciones de ese DLL cargado, donde puede llevarse a cabo el análisis. Dado que el objetivo principal de este proyecto era evitar dejar archivos en disco, colocar un DLL en disco y hacer que nuestro proceso Beacon lo cargara para tener acceso a nuestras NtFunctions de reemplazo parecía un mal enfoque. Existe un POC de hace varios años que permite cargar DLL desde memoria, lo cual es una estrategia viable para nuestras necesidades, pero el proyecto no se mantiene y parece tener varios problemas. Además, son 1200 líneas de código y sería una bestia de convertir al formato BOF.

Como breve inciso, nuestras NtFunctions de reemplazo no pueden residir en un BOF; CobaltStrike carga, ejecuta y luego borra los BOF de la memoria del proceso cuando terminan. Dado que necesitamos funciones persistentes en memoria a las que se pueda llamar cada vez que el proceso llame a una de las NtAPI hookeadas, los BOF no funcionan.

La respuesta a la que llegué fue el código independiente de posición (PIC). Como su nombre implica, en contraste con los ejecutables normales que requieren ser cargados en un lugar determinado/con partes en cierta relación con otras, el PIC puede colocarse y ejecutarse en cualquier lugar de la memoria. Esto abre la puerta a escribir nuestras NtFunctions de reemplazo como ejecutables PIC, inyectarlas en el proceso Beacon y hacer que nuestros hooks redirijan la ejecución a ellas cuando se realicen llamadas a nuestras NTAPI hookeadas.

La plantilla para estas PIC NtFunctions proviene del proyecto ShellcodeTemplate de Cracked5pider.

Una desviación notable del proyecto base es que el proyecto base está diseñado para crear un PIC exe completo; es decir, incluye ASM para guardar el puntero de pila, crear espacio en la pila, llamar a la NtFunction de reemplazo designada contenida en el exe y luego restaurar el puntero de pila una vez que esa función ha retornado. La instrucción call realizada por el PIC exe presenta un problema, ya que al hacerlo se empuja a la pila la dirección de retorno del lugar donde se hizo la llamada (en el ASM del PIC exe), lo que provocará que la instrucción ret que se encuentra más adelante devuelva la ejecución al PIC exe, en lugar de al llamador de la NtAPI original.

Para mitigar esto, el archivo ASM del proyecto ShellcodeTemplate se editó para eliminar el ASM relacionado con la configuración de la pila, la llamada a la función y la posterior restauración del puntero de pila una vez que la función ha terminado de ejecutarse. El resultado es que el hook colocado en la NtAPI ahora salta la ejecución directamente a la NtFunction de reemplazo, con la pila y los registros configurados tal y como estaban cuando el programa llamó a la NtAPI original (con la excepción de RAX, que se usa para nuestro JMP).

ASM original de ShellcodeTemplate:
image

ASM de MemFiles:
image

Cada NtAPI hookeada tiene su propia PIC NtFunction que contiene la lógica necesaria para:

A. Realizar acciones específicas de MemFiles, como crear un handle falso, escribir datos en memoria, alterar variables en el struct de MemFiles, etc.
o
B. Dirigir la ejecución a un trampolín que retome la llamada a la API y la parchee de vuelta en NTDLL, donde se pueda realizar el syscall

Algunas de las NtFunctions de reemplazo son más complejas que otras; cuando una llamada a una NtAPI hookeada le concierne a MemFiles, algunas, como NtCreateFile y NtQueryVolumeInformationFile, modifican variables que se pasaron como argumentos a la NtAPI de acuerdo con la documentación de MSDN, resultados de pruebas y algo de suposición/sentido común. Otras, como NtClose y NtReadFile, simplemente devuelven STATUS_SUCCESS al llamador original para evitar el inevitable error "Invalid Handle" que de otro modo surgiría al pasar un handle falso creado por MemFiles.

Cuando una llamada a una NtAPI hookeada NO le concierne a MemFiles, necesitamos dirigir la ejecución a un trampolín para retomar el curso:

image

Trampolines

El trampolín es responsable de ejecutar todas/cualquier instrucción que no se ejecutó en la NtAPI original debido a que esa API fue hookeada; esto incluye cualquier instrucción parcial o totalmente sobrescrita por el hook original. El hooking de APIs puede llevar rápidamente a situaciones en las que no tenemos suficiente espacio para realizar todas las instrucciones que necesitamos. Los trampolines también pueden ayudar a aliviar este problema, ya que podemos realizar cualquier cantidad de acciones para configurar nuestros registros y/o la pila antes de saltar de vuelta a la NtAPI original. El trampolín de NtCreateFile se puede ver a continuación:

image

De forma más evidente, las tres instrucciones sobrescritas por nuestro hook inicial se pueden ver como las tres primeras instrucciones del trampolín:

root@kitploit:~
MOV R10, RCX  
MOV EAX, 55  
TEST BYTE PTR DS:[7FFE0308], 1  

Como se mencionó antes, el gran requisito en toda esta manipulación es que el número de syscall (55 en el ejemplo anterior) resida en RAX(EAX) antes de emitir el syscall. Sin embargo, tenemos un problema: todavía necesitamos usar una instrucción JMP para dirigir la ejecución de vuelta a la NtAPI original. Aunque podría haber otro registro que no contenga información importante y que se pudiera aprovechar para hacerlo, no encontré una opción consistentemente segura dada la cantidad de NtAPI que estamos hookeando, cada una de las cuales podría estar usando los registros de manera diferente. La opción segura es seguir usando RAX; podemos hacerlo empujando el número de syscall almacenado en RAX a la pila. Luego podemos mover la dirección a la que deseamos saltar de vuelta en la NtAPI original a RAX y usar una instrucción JMP para regresar a NTDLL:

image

El hook instalado en la NtAPI incluye una instrucción POP RAX, y es aquí a donde saltamos usando nuestro trampolín. Ejecutar esta instrucción restaura el número de syscall en RAX desde la parte superior de la pila y nos prepara para emitir el syscall. Nótese que la instrucción JNE de la NtAPI original sin hookear sigue aquí; la instrucción TEST correspondiente, que establece el flag ZF y determina si se toma o no un JNE (lo que nos saltaría el syscall para ir al INT 2E), se realizó en el trampolín. Organizar las cosas de esta manera nos permite tanto hookear la NtAPI con éxito y redirigir la ejecución a nuestra PIC NtFunction de reemplazo, como asegurarnos de que no nos estamos saltando nada ni perdiendo funcionalidad como resultado de nuestro hooking.

El problema del carro delante del caballo

Para resumir brevemente lo cubierto hasta ahora, cuando MemFiles se inicializa, se crea un struct en la memoria del proceso Beacon que contiene información importante para la funcionalidad de MemFiles. La información en este struct se referencia continuamente a lo largo del ciclo de vida de MemFiles, incluyendo por cada una de las PIC NtFunctions de reemplazo, así como por los BOF que se utilizan para consultar y recuperar archivos almacenados en memoria por MemFiles. Con este fin, cuando se crea el struct, la dirección de memoria en la que reside el struct se comunica de vuelta al Teamserver:

image

Después de almacenar esta dirección en memtable, los comandos posteriores de MemFiles (memlist, memfetch, memclean) envían esta dirección como argumento al BOF para que el struct pueda ser localizado y referenciado. Pero, ¿cómo localizan el struct las PIC NtFunctions?

El problema evidente es que las PIC NtFunctions requieren la dirección de memoria del struct de MemFiles, pero ya están compiladas para cuando se crea el struct. Una implementación temprana de MemFiles abordó este problema dividiendo la inicialización de MemFiles en dos BOF separados. El primero creaba el struct y enviaba la dirección de vuelta al Teamserver, que mediante algo de magia de Aggressor script analizaría la dirección, la insertaría en el archivo de código fuente de cada NtFunction y luego las recompilaría en la PIC NtFunction final. El segundo BOF transmitiría entonces las PIC NtFunctions terminadas y haría la inyección y el hooking real de las NtAPI.

Además de ser feo y llevar tiempo adicional, podrían surgir problemas reales en la situación en la que múltiples beacons intentan inicializar MemFiles al mismo tiempo. Si Beacon 2 llamara de vuelta con la dirección de su struct de MemFiles mientras Beacon 1 está en el proceso de parchear y recompilar los archivos de código fuente de las NtFunctions, las cosas podrían volverse complicadas.

La solución elegante a este problema implica realizar un parche binario en la PIC NtFunction, en el cual la dirección del struct de MemFiles se parchea en la NtFunction compilada y queda accesible durante el tiempo de ejecución. Para facilitar esto, se escribe una cadena de marcador de posición en cada NtFunction:

image

Esta variable se puede ver en el código compilado usando una herramienta como xxd:

image

Cuando se ejecuta el comando meminit, cada una de las PIC NtFunctions se envía a Beacon junto con el BOF InstallHooks. Este BOF es responsable de crear el struct de MemFiles; después de hacerlo, llama a la función patchAddr en cada una de las PIC NtFunctions. patchAddr es responsable de localizar la cadena de A's y reemplazarla con la representación en cadena de la dirección del struct. En efecto, usando la dirección de memoria de la captura de pantalla anterior, la variable pFileInfoStr ahora se ve así:

char* pFileInfoStr = GET_SYMBOL( "00000178BC106860" );

Esta representación en cadena puede luego transformarse en el valor hexadecimal real, que es nuestra dirección de memoria. Los BOF logran esto de manera bastante simple usando la API _strtoi64:

image

Por supuesto, las cosas no podían ser tan fáciles para las PIC NtFunctions.

Este breve fragmento de ensamblador muestra el final de una de las PIC NtFunctions. Nótese la instrucción JMP RAX, que es la PIC NtFunction llamando al trampolín (lo que significa que es una llamada en la que MemFiles NO intervino ni manipuló):

image

Por razones desconocidas, cuando intenté usar _strtoi64 (o cualquiera de sus API primas como stroull o atoll), esta instrucción JMP RAX se convirtió en una instrucción CALL RAX. Estoy seguro de que hay una razón válida para esto que involucra algún nivel profundo del "cómo funcionan las computadoras", pero este cambio aparentemente insignificante rompe bastantes cosas. Después de pasar más de 15 horas tratando de encontrar una forma de transformar la representación en cadena de la dirección del struct de MemFiles al valor hexadecimal real para poder utilizar los valores almacenados en él, me topé con esta publicación de StackOverflow en la que un comentarista proporcionó una rutina personalizada diseñada para microcontroladores para convertir una cadena a un uint32; afortunadamente también funcionó para un uint64 sin modificación, y lo más importante, preservó la instrucción JMP RAX más adelante en la PIC NtFunction sin alterarla a un CALL. El fragmento final:

image

NtAPI antigua VS NtAPI nueva

Por simplicidad no se mencionó antes, pero el formato de las NtAPI ha cambiado a lo largo de los años. Las versiones anteriores, como las usadas por Windows 7, son mucho más cortas que sus contrapartes modernas, teniendo solo 16 bytes en lugar de 32:

image

Esto requiere cambios en la forma en que MemFiles hookea las NtAPI y en cómo se construye el trampolín. Para que MemFiles identifique la versión de las NtAPI con la que está tratando, el BOF InstallHooks primero resuelve la dirección de la NtAPI en cuestión y luego lee 32 bytes de esa ubicación. El formato diferente hace que la instrucción syscall se encuentre en lugares distintos entre las versiones de NtAPI; al comprobar la presencia o ausencia del syscall en un desplazamiento de byte determinado, MemFiles puede determinar si está tratando con la implementación moderna o la heredada de NtAPI y actuar en consecuencia:

image

Después del hooking, la API heredada NtCreateFile se ve así:

image

Y el trampolín utilizado para configurar los registros y luego saltar de vuelta a NtCreateFile:

image

En general, la técnica es muy similar, pero las cosas están mucho más ajustadas y sin mucho margen de maniobra. Vale la pena señalar que, para que todo encaje y funcione correctamente, la instrucción syscall tuvo que moverse dentro de NtCreateFile; todavía reside en el espacio de memoria de la API NtCreateFile dentro de NTDLL, pero se ha desplazado del 9º y 10º bytes de la API al 14º y 15º bytes de la API, sacrificando el NOP multibyte para poder meter todo adecuadamente.

Encontrando NtAPI relacionadas con E/S

Algunas de las API relacionadas con operaciones de E/S de archivos eran intuitivas y, por lo tanto, fáciles de identificar y hookear; otras eran mucho más esquivas, requiriendo horas y horas en WinDbg y x64dbg recorriendo el ensamblador paso a paso para intentar identificar qué API se estaban llamando. Tengo que creer que había una forma más eficiente de realizar la tarea, pero fui aprendiendo sobre la marcha.Como nota adicional, pensé en detallar el proceso —que, en retrospectiva, debería haber sido muy rápido— para identificar la NtAPI que impedía que SharpHound funcionara durante 20 horas. Al ser un programa .NET, SharpHound escupía un stack trace muy feo, todo relacionado con el hecho de que había determinado que "The handle is invalid". Si bien .NET es un lenguaje amigable para programadores, la mayoría (¿toda?) de la funcionalidad se traduce y finalmente pasa a través de la API Win32 (y, como resultado, la NtAPI) donde podemos observarla e interceptarla igual que cualquier otra cosa.

sharphounderror

El error «Invalid Handle» era una pista inequívoca de que había una NtAPI utilizada por SharpHound que no estaba enganchando (a diferencia de alguna de mis funciones Nt PIC existentes que no funcionaran correctamente), así que me propuse intentar averiguar cuál era. La técnica que había usado con otras herramientas hasta ese momento era establecer primero un punto de interrupción en NtCreateFile y localizar la llamada que concernía a mi directorio «especial». A partir de ahí, avanzaba paso a paso por el programa (normalmente varias veces porque me perdía) y observaba qué funciones llamaba el programa a continuación. Siguiendo esta metodología fue como descubrí que se llamaba a NtQueryVolumeInformationFile, NtQueryInformationFile y NtSetInformationFile, y que había que engancharlas.

SharpHound añade algunas complicaciones extra: realiza muchas de sus tareas de forma asíncrona. Esto hace mucho más difícil rastrear los pasos lineales que un solo archivo sigue a través de las llamadas API, porque hay varios archivos pasando por el proceso simultáneamente. Además, al avanzar paso a paso por el programa después de la llamada a NtCreateFile, descubrí que eventualmente el hilo en el que se realizaba dicha llamada termina; las llamadas posteriores a NtWriteFile (y cualquier otra llamada API desconocida que sea el objeto de esta búsqueda) ocurren en un hilo diferente, lo que complica aún más el proceso de búsqueda.

Habiendo tratado con .NET solo brevemente, no estaba muy versado en interpretar stack traces producidos por errores, y mucho menos en aquellos que la asincronía volvía dos veces más feos. A lo largo de las 20 horas del problema, ante la falta de avances con mi estrategia anterior, no dejaba de volver a ello y, poco a poco, le fui encontrando más sentido. Aproximadamente en el tercer «bloque» desde arriba, separados por las líneas "--- End of stack trace...", se podía ver una línea que decía "at Sharphound.Writers.JsonDataWriter...". Esto me dio un lugar relativo desde el que empezar en el código real de SharpHound, que es de código abierto y está disponible en Github. Como sugería el nombre, la función de SharpHound se encargaba de escribir la salida JSON a un archivo; ya era consciente de que mis datos no se estaban escribiendo correctamente, así que no era novedad. Subiendo un nivel en el stack trace, la siguiente línea relevante era "at System.IO.Streamwriter.". El prefijo System.IO me indicó que era algo inherente a .NET, a diferencia de una función específica de SharpHound. Mirando la sección superior del stack trace, la línea que me llamó la atención fue "at System.IO.FileStream.FlushOSBuffer()". Decidí buscar FlushOSBuffer en Google y ver qué podía encontrar.

Eso me llevó a la documentación de .NET de Microsoft para filestream.cs. Allí encontré la definición de FlushOSBuffer:

image

Parece que llama a una API Win32, FlushFileBuffers. La definición de Win32Native.FlushFileBuffers se puede encontrar consultando la documentación de Win32Native:

image

Cualquiera que haya trabajado en .NET con P/Invoke reconocerá el formato. Ya tenía una API Win32 que sabía que llama System.IO.Filestream.FlushOSBuffer(), mi problemática función .NET. Establecer un punto de interrupción en KERNEL32!FlushFileBuffers y ejecutar SharpHound lo confirmó y, al avanzar paso a paso, vi rápidamente que, bajo el capó, FlushFileBuffers llama a NtFlushBuffersFile. Enganchar esta API alivió los problemas que SharpHound estaba teniendo y le permitió ejecutarse con éxito, escribiendo sus archivos de salida en memoria.

Descargar Archivos desde la Memoria

Una parte crítica de este proyecto es la capacidad de descargar realmente los archivos al Teamserver de CobaltStrike una vez que están en memoria. Predeciblemente, el comando de descarga normal de CobaltStrike no funcionará con una ruta de archivo que realmente no existe. Sabiendo lo que sé ahora, es probable que una solución consista en desarrollar el código PIC NtReadFile de reemplazo para permitir que los archivos en memoria puedan leerse en lugar de solo escribirse. Al no tener ese conocimiento de antemano, poder recuperar realmente los archivos fue un gran obstáculo.

Por accidente me topé con un BOF escrito por EspressoCake que contenía una función que me llamó la atención:

image

Revisando el código, parece que utiliza una opción CALLBACK de Beacon no documentada:

image

La función permite que un BOF inicie la descarga de un archivo al Teamserver desde el objetivo, a diferencia de hacerlo desde el Cliente. Esta capacidad (que más tarde supe que fue un esfuerzo combinado de varias otras personas, incluyendo a @Cr0Eax, @EthicalChaos y @anthemtotheego) eliminó el gran obstáculo que existía antes, ya que ahora tenía una forma de iniciar una transferencia de archivos desde el sistema objetivo para el archivo en memoria. Muchas gracias a todos los involucrados por este fragmento de código, que preveo que también será útil en el futuro.

Estructura de Datos de MemFiles

Una parte desafiante de este proyecto fue garantizar la disponibilidad de la funcionalidad de MemFiles para todos los Clientes de CobaltStrike conectados al Team Server. Los datos de MemFiles se almacenan en estructuras creadas por MemFiles.cna, que deben cargarse en cada Cliente que desee usar la herramienta; como resultado, estas estructuras de datos viven dentro de cada Cliente, no en el Team Server. Si estos datos vivieran en una única ubicación central (TS), sería trivial recuperarlos desde cada Cliente y todo esto no sería un problema; si el equipo de CobaltStrike integrara formalmente una capacidad como MemFiles en CobaltStrike, estoy seguro de que esa sería la dirección que tomarían. Pero como se trata de un complemento de la comunidad, nos arreglamos con lo que tenemos.

Hay un par de escenarios diferentes que debemos tener en cuenta para asegurar que cada Cliente de CobaltStrike tenga los datos más recientes y precisos sobre el estado de MemFiles dentro de los Beacons y la configuración:

Nuevos Clientes que se conectan al TS y necesitan la memtable actual
Casos en los que solo un Cliente está conectado al TS y reinicia CobaltStrike (perdiendo así la memtable almacenada en la memoria del Cliente)
El Cliente A realiza un cambio en los datos de MemFiles que debe comunicarse al Cliente B

Se adoptó un enfoque multifacético para abordar estos escenarios. Para manejar el caso en el que solo un Cliente de CobaltStrike está conectado al TS (y por lo tanto es la única entidad que tiene los datos de la memtable), cada vez que el Cliente modifica la memtable (meminit, memclean) también escribe el contenido de su memtable en un archivo de texto local ubicado en el directorio de CobaltStrike. Si el Cliente sale/reinicia, o cuando se recarga MemFiles.cna, primero intentará leer el archivo local memtable.txt para poblar su memtable en memoria.

Cuando hay varios Clientes conectados a un TS y un nuevo Cliente se une (según el Event Log), cada Cliente obtiene una lista de todos los usuarios conectados al TS y la ordena alfabéticamente. El Cliente que aparece primero en esa lista se selecciona como el Cliente "Broadcast" y, después de esperar 5 segundos (para permitir que el nuevo Cliente se inicialice y lea su memtable.txt local), envía mensajes (Actions) en el Event Log por cada entrada en su memtable. Todos los Clientes (excepto el que transmite) leen estos mensajes y actualizan su memtable con la información transmitida; esto incluye actualizar las entradas existentes, así como agregar cualquier entrada adicional que su respectiva memtable no contenga.

Las operaciones normales que involucran a MemFiles también dependen del envío de mensajes en el Event Log. Cuando el Cliente A ejecuta meminit, se transmite un mensaje que contiene toda la información pertinente de la memtable; TODOS los Clientes actualizan su respectiva memtable analizando estos mensajes del Event Log mediante el hook "on Event_Action". También se realizan cambios en los datos de MemFiles cuando meminit termina de ejecutar su BOF; estos cambios son comunicados de vuelta por el Beacon (por ejemplo, después de ejecutar meminit, el Beacon responde con la ubicación en memoria de la estructura pMemAddrs) y, como tales, son visibles para todos los Clientes conectados, que actualizan su respectiva memtable usando el hook "on Beacon_Output".

Estos esfuerzos separados combinados dan como resultado que MemFiles pueda sincronizar de manera eficiente y confiable datos críticos entre múltiples Clientes.

Créditos y Agradecimientos

Este proyecto no sería posible sin las contribuciones de las siguientes personas y proyectos:

  1. x64-NTAPI-inline-hook por globalpolicy
  2. x64 Function Hooking by Example por Kyle Halladay
  3. ShellcodeTemplate por Cracked5pider, también conocido como @C5pider
  4. @ilove2pwn_, por mediación de Cracked5pider
  5. DLL-Exports-Extraction-BOF por EspressoCake, también conocido como @the_bit_diddler
  6. @anthemtotheego, por mediación de EspressoCake
  7. @Cr0Eax, por mediación de @anthemtotheego
  8. @EthicalChaos, por mediación de @anthemtotheego
  9. SysWhispers is dead, long live SysWhispers! por KlezVirus
  10. @yarden_shafir
  11. @DexterGerig
Descargar herramienta