Evasión de Firmas
Última actualización
Última actualización
Antes de lanzarnos a romper firmas, debemos comprender e identificar lo que estamos buscando. Como se explica en , los motores antivirus utilizan firmas para rastrear e identificar posibles programas sospechosos y/o maliciosos. En esta tarea observaremos cómo podemos identificar manualmente un byte exacto donde comienza una firma.
Al identificar firmas, ya sea manual o automatizada, debemos emplear un proceso iterativo para determinar en qué byte comienza una firma. Al dividir recursivamente un binario compilado por la mitad y probarlo, podemos obtener una estimación aproximada de un rango de bytes para investigar más a fondo.
Podemos usar las utilidades nativas head
, dd
o split
para dividir un binario compilado. En el siguiente símbolo del sistema, usaremos head para encontrar la primera firma presente en un binario msfvenom.
Una vez dividido, mueva el binario desde su entorno de desarrollo a una máquina con el motor antivirus en el que desea realizar la prueba. Si aparece una alerta, muévase a la mitad inferior del binario dividido y divídalo nuevamente. Si no aparece una alerta, vaya a la mitad superior del binario dividido y divídalo nuevamente. Continúe este patrón hasta que no pueda determinar adónde ir; Esto normalmente ocurrirá alrededor del rango de kilobytes.
Una vez que haya llegado al punto en el que ya no puede dividir con precisión el binario, puede utilizar un editor hexadecimal para ver el final del binario donde está presente la firma.
Tenemos la ubicación de una firma; qué tan legible sea para humanos estará determinado por la propia herramienta y el método de compilación.
Ahora… nadie quiere pasar horas yendo y viniendo tratando de localizar bytes defectuosos; ¡automaticémoslo! En la siguiente tarea, veremos algunas soluciones FOSS ( software gratuito y de código abierto ) que nos ayudarán a identificar firmas en el código compilado .
Este script alivia gran parte del trabajo manual, pero aún tiene varias limitaciones. Aunque requiere menos interacción que la tarea anterior, aún requiere establecer un intervalo adecuado para funcionar correctamente. Este script también observará únicamente cadenas del binario cuando se coloque en el disco en lugar de escanear utilizando la funcionalidad completa del motor antivirus.
ThreatCheck es una bifurcación de DefenderCheck y es posiblemente el más utilizado y confiable de los tres. Para identificar posibles firmas, ThreatCheck aprovecha varios motores antivirus contra archivos binarios compilados divididos e informa donde cree que hay bytes defectuosos.
ThreatCheck no proporciona al público una versión precompilada. Para facilitar su uso, ya hemos compilado la herramienta para usted; se puede encontrar en C:\Users\Administrator\Desktop\Tools
la máquina adjunta.
A continuación se muestra el uso de sintaxis básica de ThreatCheck.
Para nuestros usos sólo necesitamos suministrar un archivo y opcionalmente un motor; sin embargo, principalmente querremos utilizar AMSITrigger cuando trabajemos con AMSI ( A nti- M alware S can Interface ), como veremos más adelante en esta tarea.
¡Es así de simple! No se requiere ninguna otra configuración o sintaxis y podemos pasar directamente a modificar nuestras herramientas. Para utilizar esta herramienta de manera eficiente, podemos identificar los bytes defectuosos que se descubren primero, luego dividirlos de forma recursiva y ejecutar la herramienta nuevamente hasta que no se identifiquen firmas.
Nota: Puede haber casos de falsos positivos, en los que la herramienta no informará bytes incorrectos. Esto requerirá de tu propia intuición para observar y resolver
AMSI Trigger aprovechará el motor AMSI y las funciones de escaneo contra un script de PowerShell proporcionado e informará cualquier sección específica de código sobre la que crea que debe recibir alertas.
AMSITrigger proporciona una versión precompilada en su GitHub y también se puede encontrar en el escritorio de la máquina adjunta.
A continuación se muestra el uso de sintaxis de AMSITrigger.
Para nuestros usos sólo necesitamos proporcionar un archivo y el formato preferido para reportar firmas.
En la siguiente tarea, analizaremos cómo puede utilizar la información recopilada de estas herramientas para romper firmas.
Método de ofuscación
Objetivo
Proxy de método(Method Proxy)
Crea un método proxy o un objeto de reemplazo.
Método de dispersión/agregación(Method Scattering/Aggregation)
Combine varios métodos en uno o disperse un método en varios
Clonar método(Method Clone)
Crear réplicas de un método y llamar aleatoriamente a cada una
Método de ofuscación
Objetivo
Aplanamiento de la jerarquía de clases(Class Hierarchy Flattening)
Crear servidores proxy para clases usando interfaces
División/fusión de clases(Class Splitting/Coalescing)
Transferir variables locales o grupos de instrucciones a otra clase.
Eliminación de modificadores(Dropping Modifiers)
Eliminar modificadores de clase (público, privado) y hacer públicos a todos los miembros
Al observar las tablas anteriores, aunque pueden utilizar términos o ideas técnicas específicas, podemos agruparlas en un conjunto central de métodos agnósticos aplicables a cualquier objeto o estructura de datos.
Las técnicas de división/fusión de clases y dispersión /agregación de métodos se pueden agrupar en un concepto general de división o fusión de cualquier función POO (programación orientada a objetos ) determinada .
Otras técnicas, como eliminar modificadores o clonar métodos , se pueden agrupar en un concepto general de eliminar u ocultar información identificable.
La premisa detrás de este concepto es relativamente fácil: buscamos crear una nueva función de objeto que pueda romper la firma manteniendo la funcionalidad anterior.
Cadena original
A continuación se muestra la cadena original que se detecta.
Método ofuscado
A continuación se muestra la nueva clase utilizada para reemplazar y concatenar la cadena.
Recapitulando este caso de estudio, la división de clases se utiliza para crear una nueva clase para concatenar la variable local. Cubriremos cómo reconocer cuándo utilizar un método específico más adelante en esta tarea y durante todo el desafío práctico.
Un ejemplo de esto lo podemos encontrar en Mimikatz donde se genera una alerta para la cadena wdigest.dll
. Esto se puede resolver reemplazando la cadena con cualquier identificador aleatorio modificado en todas las instancias de la cadena. Esto se puede clasificar en la taxonomía de ofuscación bajo la técnica de proxy del método.
Utilizando el conocimiento que ha acumulado a lo largo de esta tarea, oculte el siguiente fragmento de PowerShell utilizando AmsiTrigger para firmas visuales.
Varios motores de detección o analistas pueden considerar diferentes indicadores en lugar de cadenas o firmas estáticas para contribuir a sus hipótesis. Las firmas se pueden adjuntar a varias propiedades de archivos, incluido el hash del archivo, la entropía , el autor, el nombre u otra información identificable para usar individualmente o en conjunto. Estas propiedades se utilizan a menudo en conjuntos de reglas como YARA o Sigma .
Algunas propiedades pueden manipularse fácilmente, mientras que otras pueden ser más difíciles, específicamente cuando se trata de aplicaciones de código cerrado precompiladas.
Esta tarea analizará la manipulación del hash de archivos y la entropía de aplicaciones de código abierto y cerrado.
Nota: varias otras propiedades, como encabezados PE o propiedades de módulo, se pueden utilizar como indicadores. Debido a que estas propiedades a menudo requieren un agente u otras medidas para detectarlas, no las cubriremos en esta sala para centrarnos en las firmas.
Un hash de archivo , también conocido como suma de comprobación , se utiliza para etiquetar/identificar un archivo único. Se utilizan comúnmente para verificar la autenticidad de un archivo o su propósito conocido (malicioso o no). Los hashes de archivos generalmente son arbitrarios de modificar y se modifican debido a cualquier modificación del archivo.
Si tenemos acceso al código fuente de una aplicación, podemos modificar cualquier sección arbitraria del código y volver a compilarla para crear un nuevo hash. Esa solución es sencilla, pero ¿qué pasa si necesitamos una aplicación precompilada o firmada?
Cuando se trata de una aplicación firmada o de código cerrado, debemos emplear el cambio de bits .
El cambio de bits es un ataque criptográfico común que mutará una aplicación determinada volteando y probando cada bit posible hasta que encuentre un bit viable. Al invertir un bit viable, se cambiará la firma y el hash de la aplicación manteniendo todas las funciones.
Podemos usar un script para crear una lista de bits invertidos invirtiendo cada bit y creando una nueva variante mutada (~3000 - 200000 variantes). A continuación se muestra un ejemplo de una implementación de inversión de bits en Python.
Una vez creada la lista, debemos buscar propiedades únicas intactas del archivo. Por ejemplo, si estamos invirtiendo bits msbuild
, debemos usar signtool
para buscar un archivo con un certificado utilizable. Esto garantizará que la funcionalidad del archivo no se interrumpa y la aplicación mantendrá su atribución firmada.
Podemos aprovechar un script para recorrer la lista de bits invertidos y verificar variantes funcionales. A continuación se muestra un ejemplo de una implementación de script por lotes.
Esta técnica puede resultar muy lucrativa, aunque puede llevar mucho tiempo y sólo tendrá un periodo limitado hasta que se descubra el hash. A continuación se muestra una comparación de la aplicación MSBuild original y la variación de bits invertidos.
La entropía puede ser problemática para scripts ofuscados, específicamente cuando se oculta información identificable como variables o funciones.
Para reducir la entropía , podemos reemplazar los identificadores aleatorios con palabras en inglés seleccionadas al azar. Por ejemplo, podemos cambiar una variable deq234uf
a nature
.
A continuación se muestra la escala de entropía de Shannon para un párrafo estándar en inglés.
Entropía de Shannon : 4.587362034903882
A continuación se muestra la escala de entropía de Shannon para un script pequeño con identificadores aleatorios.
Entropía de Shannon : 5.341436973971389
Dependiendo del EDR empleado, un valor de entropía "sospechoso" es ~ mayor que 6,8 .
La diferencia entre un valor aleatorio y un texto en inglés se amplificará con un archivo más grande y más apariciones.
Tenga en cuenta que la entropía generalmente nunca se utilizará sola y sólo para respaldar una hipótesis. Por ejemplo, la entropía del comandopskill
y el exploit hivennightmare son casi idénticas.
Para ver la entropía en acción, veamos cómo la usaría un EDR para contribuir a los indicadores de amenaza.
Ofuscar funciones y propiedades puede lograr mucho con una modificación mínima. Incluso después de romper las firmas estáticas adjuntas a un archivo, los motores modernos aún pueden observar el comportamiento y la funcionalidad del binario. Esto presenta numerosos problemas para los atacantes que no pueden resolverse con una simple ofuscación.
Antes de profundizar demasiado en la reescritura o importación de llamadas, analicemos cómo se utilizan e importan tradicionalmente las llamadas API . Primero cubriremos los lenguajes basados en C y luego cubriremos brevemente los lenguajes basados en .NET más adelante en esta tarea.
Las llamadas API y otras funciones nativas de un sistema operativo requieren un puntero a una dirección de función y una estructura para utilizarlas.
Las estructuras de funciones son simples; se encuentran en bibliotecas de importación como kernel32
o ntdll
que almacenan estructuras de funciones y otra información básica para Windows.
El problema más importante para las importaciones de funciones son las direcciones de funciones. Obtener un puntero puede parecer sencillo, aunque debido a ASLR ( Aleatorización del diseño del espacio de direcciones ), las direcciones de las funciones son dinámicas y deben encontrarse.
En lugar de alterar el código en tiempo de ejecución, se emplea el cargador de Windows . windows.h
En tiempo de ejecución, el cargador asignará todos los módulos para procesar el espacio de direcciones y enumerará todas las funciones de cada uno. Eso maneja los módulos, pero ¿cómo se asignan las direcciones de función?
Una de las funciones más críticas del cargador de Windows es la IAT ( tabla de direcciones de importación ). El IAT almacenará direcciones de funciones para todas las funciones importadas que puedan asignar un puntero a la función.
De un vistazo, a una API se le asigna un puntero a un procesador como dirección de función desde el cargador de Windows. Para hacer esto un poco más tangible, podemos observar un ejemplo del volcado PE de una función.
La tabla de importación puede proporcionar mucha información sobre la funcionalidad de un binario que puede ser perjudicial para un adversario. Pero, ¿cómo podemos evitar que nuestras funciones aparezcan en el IAT si se requiere asignar una dirección de función?
Como se mencionó brevemente, la tabla de procesadores no es la única forma de obtener un puntero para la dirección de una función. También podemos utilizar una llamada API para obtener la dirección de la función de la propia biblioteca de importación. Esta técnica se conoce como carga dinámica y se puede utilizar para evitar el IAT y minimizar el uso del cargador de Windows.
Escribiremos nuestras estructuras y crearemos nuevos nombres arbitrarios para funciones que empleen carga dinámica.
En un nivel alto, podemos dividir la carga dinámica en lenguajes C en cuatro pasos,
Definir la estructura de la convocatoria.
Obtenga el identificador del módulo en el que está presente la dirección de llamada
Obtener la dirección del proceso de la llamada.
Utilice la llamada recién creada
Para acceder a la dirección de la llamada API , primero debemos cargar la biblioteca donde está definida. Definiremos esto en la función principal. Esto es comúnkernel32.dll
o ntdll.dll
para cualquier llamada a la API de Windows . A continuación se muestra un ejemplo de la sintaxis necesaria para cargar una biblioteca en un identificador de módulo.
Usando el módulo previamente cargado, podemos obtener la dirección del proceso para la llamada API especificada . Esto vendrá inmediatamente después de la LoadLibrary
llamada. Podemos almacenar esta llamada transmitiéndola junto con la estructura previamente definida. A continuación se muestra un ejemplo de la sintaxis necesaria para obtener la llamada API .
Aunque este método resuelve muchas inquietudes y problemas, todavía hay varias consideraciones que se deben tener en cuenta. En primer lugar, GetProcAddress
y LoadLibraryA
siguen presentes en el IAT; aunque no es un indicador directo, puede generar sospechas o reforzarlas; Este problema se puede resolver usando PIC ( Código independiente de posición ) . Los agentes modernos también conectarán funciones específicas y monitorearán las interacciones del núcleo; Esto se puede resolver usando API desenganche .
Utilizando el conocimiento que ha acumulado a lo largo de esta tarea, oculte el siguiente fragmento de C, asegurándose de que no haya llamadas API sospechosas en el IAT.
Para crear una metodología más eficaz y fiable, podemos combinar varios de los métodos tratados en esta sala y en la anterior.
Al determinar en qué orden desea comenzar la ofuscación, considere el impacto de cada método. Por ejemplo, ¿es más fácil ofuscar una clase que ya está rota o es más fácil ofuscar una clase que está ofuscada?
Nota: En general, debe ejecutar la ofuscación automatizada o métodos de ofuscación menos específicos después de romper una firma específica; sin embargo, no necesitará esas técnicas para este desafío.
Teniendo en cuenta estas notas, modifique el binario proporcionado para cumplir con las especificaciones siguientes.
No hay llamadas sospechosas a la biblioteca presentes
No se han filtrado funciones ni nombres de variables.
El hash del archivo es diferente al hash original
Binary evita los motores antivirus comunes
Nota: Al considerar las llamadas a la biblioteca y las funciones filtradas, tenga en cuenta la tabla IAT y las cadenas de su binario.
Nota: También es esencial cambiar las variables C2Server
y C2Port
en la carga útil proporcionada o este desafío no funcionará correctamente y no recibirá un shell de vuelta.
Nota: Al compilar con GCC necesitarás agregar opciones de compilación para winsock2
y ws2tcpip
. Estas bibliotecas se pueden incluir utilizando los indicadores del compilador -lwsock32
y-lws2_32
Si todavía está atascado, le proporcionamos un tutorial de la solución a continuación.
Para este desafío, se nos proporciona un binario que no creamos. Nuestro primer objetivo es familiarizarnos con el binario desde la perspectiva de la ingeniería inversa. ¿Hay firmas? ¿ Cómo es su estructura de PE ? ¿Hay alguna información crítica en el IAT?
Si ejecuta el binario con ThreatCheck o una herramienta similar, notará que actualmente no tiene detecciones, por lo que podemos continuar con eso.
Si inspecciona la tabla IAT de binarios como se explicó en la tarea 6, notará que hay aproximadamente siete llamadas API únicas que podrían indicar los objetivos de este binario.
Revisaremos cómo llamar dinámicamente a una llamada API y luego esperaremos que reitere los pasos para el resto de las llamadas.
Ahora podemos reescribir esto para cumplir con los requisitos de una definición de estructura.
Ahora necesitamos importar la biblioteca en la que están almacenadas las llamadas. Esto solo debe ocurrir una vez ya que todas las llamadas usan la misma biblioteca.
Para utilizar la llamada API , debemos obtener el puntero a la dirección.
Una vez obtenido el puntero, podemos cambiar todas las apariciones de la llamada API con nuestro nuevo puntero.
Una vez completada, la definición de su estructura debería verse como el siguiente fragmento de código
El siguiente fragmento de código define todas las direcciones de puntero necesarias, correspondientes a las estructuras anteriores.
Tenga en cuenta que las definiciones de la estructura deben estar fuera de cualquier función al comienzo de su código. Las definiciones de puntero deben estar en la parte superior de la RunShell
función.
Ahora deberíamos aleatorizar el puntero y otros nombres de variables siguiendo las mejores prácticas adecuadas. También deberíamos despojar al binario de cualquier símbolo u otra información identificable.
Una vez que se haya ofuscado completamente y se haya eliminado la información, podemos compilar el binario usando mingw-gcc
.
El proceso que se muestra en la tarea anterior puede resultar bastante arduo. Para acelerarlo, podemos automatizarlo usando scripts para dividir bytes en un intervalo para nosotros. dividirá un rango de bytes proporcionado en un intervalo determinado.
Para resolver este problema podemos utilizar otras FOSS ( software gratuito y de código abierto ) que aprovechan los propios motores para escanear el archivo, incluidos DefenderCheck , y . En esta tarea, nos centraremos principalmente en ThreatCheck y mencionaremos brevemente los usos de AMSITrigger al final.
Como se explica en , AMSI aprovecha el tiempo de ejecución, lo que hace que las firmas sean más difíciles de identificar y resolver. ThreatCheck tampoco admite ciertos tipos de archivos, como PowerShell, que sí admite AMSITrigger.
Una vez que hayamos identificado una firma problemática, debemos decidir cómo queremos abordarla. Dependiendo de la fuerza y el tipo de firma, se puede romper mediante una simple ofuscación, como se describe en , o puede requerir una investigación y una solución específicas. En esta tarea, nuestro objetivo es proporcionar varias soluciones para remediar las firmas estáticas presentes en las funciones.
La cubre las soluciones más confiables como parte de la capa Métodos de ofuscación y Clases de ofuscación .
La metodología requerida para dividir o fusionar objetos es muy similar al objetivo de concatenación como se describe en los
Para proporcionar un ejemplo más concreto de esto, podemos utilizar el de Covenant presente en la GetMessageFormat
cadena. Primero veremos cómo se implementó la solución, luego la desglosaremos y la aplicaremos a la taxonomía de ofuscación.
El concepto central detrás de la eliminación de información identificable es similar a ocultar nombres de variables como se describe en . En esta tarea, vamos un paso más allá al aplicarlo específicamente a firmas identificadas en cualquier objeto, incluidos métodos y clases.
Esto casi no es diferente de lo discutido en ; sin embargo, se aplica a una situación específica.
Desde , la entropía se define como “la aleatoriedad de los datos de un archivo que se utiliza para determinar si un archivo contiene datos ocultos o scripts sospechosos”. Los EDR y otros escáneres a menudo aprovechan la entropía para identificar posibles archivos sospechosos o contribuir a una puntuación maliciosa general.
Para demostrar la eficacia de cambiar identificadores, podemos observar cómo cambia la entropía usando .
En el documento técnico, , se muestra que SentinelOne detecta una DLL debido a una alta entropía, específicamente a través del cifrado AES.
Como se explica en , los motores antivirus modernos emplearán dos métodos comunes para detectar el comportamiento: observar las importaciones y conectar llamadas maliciosas conocidas. Si bien las importaciones, como se cubrirá en esta tarea, se pueden ofuscar o modificar fácilmente con requisitos mínimos, el enlace requiere técnicas complejas fuera del alcance de esta sala. Debido a la prevalencia específica de las llamadas API , observar estas funciones puede ser un factor importante para determinar si un archivo es sospechoso, junto con otras pruebas/consideraciones de comportamiento.
El IAT se almacena en el encabezado PE ( P ortable Executable ) IMAGE_OPTIONAL_HEADER
y el cargador de Windows lo completa en tiempo de ejecución. El cargador de Windows obtiene las direcciones de función o, más precisamente, los procesadores de una tabla de punteros, a la que se accede desde una llamada API o una tabla de procesadores . Consulte la para obtener más información sobre la estructura de PE .
Para comenzar a cargar dinámicamente una llamada API , primero debemos definir una estructura para la llamada antes de la función principal. La estructura de la llamada definirá cualquier entrada o salida que pueda ser necesaria para que la llamada funcione. Podemos encontrar estructuras para una llamada específica en la documentación de Microsoft. Por ejemplo, la estructura deGetComputerNameA
se puede encontrar . Debido a que estamos implementando esto como una nueva llamada en C, la sintaxis debe cambiar un poco, pero la estructura sigue siendo la misma, como se ve a continuación.
Como se reitera tanto en esta sala como en , ningún método será 100% efectivo o confiable.
Una vez que esté lo suficientemente ofuscado, compile la carga útil en AttackBox o VM de su elección usando GCC u otro compilador de C. El nombre del archivo debe guardarse comochallenge.exe
. Una vez compilado, envíe el ejecutable al servidor web en http://MACHINE_IP/
Si su carga útil cumple con los requisitos enumerados, se ejecutará y se enviará una baliza a la IP y al puerto del servidor proporcionado.
Concentremos nuestros esfuerzos en eliminarlos de la tabla IAT y llamarlos dinámicamente. Para resumir lo que se cubrió en la tarea 6: necesitamos identificar una llamada API , cargar la biblioteca para las llamadas API y obtener un puntero a la llamada API.
Miremos el para WSAConnect
; A continuación se muestra la estructura obtenida de la documentación.