Abusing Windows Internal
Última actualización
Última actualización
Las aplicaciones que se ejecutan en su sistema operativo pueden contener uno o más procesos. Los procesos mantienen y representan un programa que se está ejecutando.
Los procesos tienen muchos otros subcomponentes e interactúan directamente con la memoria o la memoria virtual, lo que los convierte en un candidato perfecto para apuntar. La siguiente tabla describe cada componente crítico de los procesos y su propósito.
Componente del proceso
Objetivo
Private Virtual Address Space
Direcciones de memoria virtual que se asignan al proceso.
Executable Program
Define el código y los datos almacenados en el espacio de direcciones virtuales.
Open Handles
Define identificadores de recursos del sistema accesibles al proceso.
Security Context
El token de acceso define el usuario, los grupos de seguridad, los privilegios y otra información de seguridad.
Process ID
Identificador numérico único del proceso.
Threads
Sección de un proceso programado para su ejecución
La inyección de procesos se utiliza comúnmente como término general para describir la inyección de código malicioso en un proceso a través de funciones o componentes legítimos. En esta sala nos centraremos en cuatro tipos diferentes de inyección de procesos, que se describen a continuación.
Tipo de inyección
Función
Inyectar código en un proceso de destino suspendido y "hueco"
Inyectar código en un hilo de destino suspendido
Inyectar una DLL en la memoria del proceso
Auto inyectar una imagen PE que apunta a una función maliciosa en un proceso de destino
Hay muchas otras formas de inyección de proceso descritas en .
En su nivel más básico, la inyección de procesos toma la forma de inyección de código shell.
En un nivel alto, la inyección de shellcode se puede dividir en cuatro pasos:
Abra un proceso de destino con todos los derechos de acceso.
Asigne memoria de proceso de destino para el código de shell.
Escriba shellcode en la memoria asignada en el proceso de destino.
Ejecute el código shell usando un hilo remoto.
Los pasos también se pueden desglosar gráficamente para representar cómo las llamadas a la API de Windows interactúan con la memoria del proceso.
Desglosaremos un inyector de shellcode básico para identificar cada uno de los pasos y explicarlo con más profundidad a continuación.
En el paso uno de la inyección de shellcode, necesitamos abrir un proceso de destino utilizando parámetros especiales. OpenProcess
se utiliza para abrir el proceso de destino proporcionado a través de la línea de comandos.
En el paso dos, debemos asignar memoria al tamaño de bytes del código shell. La asignación de memoria se maneja mediante VirtualAllocEx
. Dentro de la llamada, el dwSize
parámetro se define usando la sizeof
función para obtener los bytes de shellcode que se asignarán.
En el paso tres, ahora podemos usar la región de memoria asignada para escribir nuestro código shell. WriteProcessMemory
se usa comúnmente para escribir en regiones de memoria.
En el paso cuatro, ahora tenemos el control del proceso y nuestro código malicioso ahora está escrito en la memoria. Para ejecutar el código shell que reside en la memoria, podemos usar CreateRemoteThread
; Los hilos controlan la ejecución de los procesos.
Podemos compilar estos pasos juntos para crear un inyector de proceso básico. Utilice el inyector de C++ proporcionado y experimente con la inyección de procesos.
La inyección de Shellcode es la forma más básica de inyección de procesos; En la siguiente tarea, veremos cómo podemos modificar y adaptar estos pasos para el vaciado del proceso.
En la tarea anterior, analizamos cómo podemos utilizar la inyección de shellcode para inyectar código malicioso en un proceso legítimo. En esta tarea cubriremos el proceso de vaciado. Similar a la inyección de shellcode, esta técnica ofrece la posibilidad de inyectar un archivo malicioso completo en un proceso. Esto se logra “vaciando” o desasignando el proceso e inyectando secciones y datos PE ( portátiles ejecutables ) específicos en el proceso.
En un proceso de alto nivel, el vaciado se puede dividir en seis pasos:
Cree un proceso de destino en estado suspendido.
Abra una imagen maliciosa.
Desasignar código legítimo de la memoria del proceso.
Asigne ubicaciones de memoria para códigos maliciosos y escriba cada sección en el espacio de direcciones.
Establezca un punto de entrada para el código malicioso.
Saque el proceso objetivo del estado suspendido.
Los pasos también se pueden desglosar gráficamente para representar cómo las llamadas a la API de Windows interactúan con la memoria del proceso.
Desglosaremos un proceso básico de vaciado de inyectores para identificar cada uno de los pasos y explicarlo con más profundidad a continuación.
En el paso uno del vaciado del proceso, debemos crear un proceso objetivo en estado suspendido usando CreateProcessA
. Para obtener los parámetros requeridos para la llamada API podemos usar las estructuras STARTUPINFOA
y PROCESS_INFORMATION
.
En el paso dos, necesitamos abrir una imagen maliciosa para inyectarla. Este proceso se divide en tres pasos, comenzando por utilizar CreateFileA
para obtener un identificador de la imagen maliciosa.
Una vez que se obtiene un identificador para la imagen maliciosa, se debe asignar memoria al proceso local usando VirtualAlloc
. GetFileSize
También se utiliza para recuperar el tamaño de la imagen maliciosa para dwSize
.
Ahora que la memoria está asignada al proceso local, se debe escribir. Usando la información obtenida en los pasos anteriores, podemos usarla ReadFile
para escribir en la memoria del proceso local.
En el paso tres, el proceso debe ser “vaciado” desasignando la memoria. Antes de que se pueda eliminar la asignación, debemos identificar los parámetros de la llamada API. Necesitamos identificar la ubicación del proceso en la memoria y el punto de entrada. Los registros de la CPU EAX
(punto de entrada) y EBX
(ubicación PEB) contienen la información que necesitamos obtener; estos se pueden encontrar usando GetThreadContext
. Una vez encontrados ambos registros, ReadProcessMemory
se utiliza para obtener la dirección base del EBX
con un desplazamiento ( 0x8
), obtenido al examinar el PEB.
Una vez almacenada la dirección base, podemos comenzar a desasignar la memoria. Podemos utilizar ZwUnmapViewOfSection
importado desde ntdll.dll para liberar memoria del proceso de destino.
En el paso cuatro, debemos comenzar asignando memoria en el proceso vaciado. Podemos usar VirtualAlloc
algo similar al paso dos para asignar memoria. Esta vez necesitamos obtener el tamaño de la imagen que se encuentra en los encabezados de los archivos. e_lfanew
Puede identificar el número de bytes desde el encabezado de DOS hasta el encabezado de PE. Una vez en el encabezado PE, podemos obtenerlo SizeOfImage
del encabezado Opcional.
Una vez asignada la memoria, podemos escribir el archivo malicioso en la memoria. Debido a que estamos escribiendo un archivo, primero debemos escribir los encabezados PE y luego las secciones PE. Para escribir encabezados PE, podemos usar WriteProcessMemory
y el tamaño de los encabezados para determinar dónde detenernos.
Ahora necesitamos escribir cada sección. Para encontrar el número de secciones, podemos utilizar NumberOfSections
los encabezados de NT. Podemos recorrer e_lfanew
el tamaño del encabezado actual para escribir cada sección.
También es posible utilizar tablas de reubicación para escribir el archivo en la memoria de destino. Esto se discutirá con más profundidad en la tarea 6.
En el paso cinco, podemos usar SetThreadContext
para cambiar EAX
para apuntar al punto de entrada.
En el paso seis, debemos sacar el proceso del estado suspendido usando ResumeThread
.
Podemos compilar estos pasos juntos para crear un inyector hueco de proceso. Utilice el inyector de C++ proporcionado y experimente con el vaciado del proceso.
En un hilo de alto nivel (ejecución), el secuestro se puede dividir en once pasos:
Localice y abra un proceso objetivo para controlar.
Asigne una región de memoria para código malicioso.
Escriba código malicioso en la memoria asignada.
Identifique el ID del hilo de destino que se va a secuestrar.
Abra el hilo de destino.
Suspender el hilo de destino.
Obtenga el contexto del hilo.
Actualice el puntero de instrucción al código malicioso.
Reescribe el contexto del hilo de destino.
Reanudar el hilo secuestrado.
Desglosaremos un script básico de secuestro de hilos para identificar cada uno de los pasos y explicarlo con más profundidad a continuación.
Los primeros tres pasos descritos en esta técnica siguen los mismos pasos comunes que el proceso de inyección normal. Estos no se explicarán; en cambio, puede encontrar el código fuente documentado a continuación.
Una vez que hayamos realizado los pasos iniciales y nuestro código shell esté escrito en la memoria, podemos pasar al paso cuatro. En el paso cuatro, debemos comenzar el proceso de secuestrar el hilo del proceso identificando el ID del hilo. Para identificar el ID del hilo, necesitamos usar un trío de llamadas a la API de Windows: CreateToolhelp32Snapshot()
, Thread32First()
y Thread32Next()
. Estas llamadas API recorrerán colectivamente una instantánea de un proceso y ampliarán las capacidades para enumerar la información del proceso.
En el paso cinco, hemos recopilado toda la información requerida en el puntero de estructura y podemos abrir el hilo de destino. Para abrir el hilo usaremos OpenThread
el THREADENTRY32
puntero de estructura.
En el paso seis, debemos suspender el hilo de destino abierto. Para suspender el hilo podemos usar SuspendThread
.
En el paso siete, necesitamos obtener el contexto del hilo para usarlo en las próximas llamadas API. Esto se puede hacer usando GetThreadContext
para almacenar un puntero.
En el paso ocho, necesitamos sobrescribir RIP (Registro de puntero de instrucción) para que apunte a nuestra región maliciosa de memoria. Si aún no está familiarizado con los registros de la CPU, RIP es un registro x64 que determinará la siguiente instrucción de código; En pocas palabras, controla el flujo de una aplicación en la memoria. Para sobrescribir el registro podemos actualizar el contexto del hilo para RIP.
En el paso nueve, el contexto se actualiza y debe actualizarse al contexto del hilo actual. Esto se puede hacer fácilmente usando SetThreadContext
y el puntero del contexto.
En el paso final, ahora podemos sacar el hilo de destino del estado suspendido. Para lograr esto podemos usar ResumeThread
.
Podemos compilar estos pasos juntos para crear un inyector de proceso mediante el secuestro de subprocesos. Utilice el inyector de C++ proporcionado y experimente con el secuestro de subprocesos.
En un nivel alto, la inyección de DLL se puede dividir en seis pasos:
Localice un proceso de destino para inyectar.
Abra el proceso de destino.
Asigne una región de memoria para DLL maliciosa.
Escriba la DLL maliciosa en la memoria asignada.
Cargue y ejecute la DLL maliciosa.
Desglosaremos un inyector de DLL básico para identificar cada uno de los pasos y explicarlo con más profundidad a continuación.
En el paso uno de la inyección de DLL, debemos localizar un hilo de destino. Se puede localizar un hilo desde un proceso utilizando un trío de llamadas a la API de Windows: CreateToolhelp32Snapshot()
, Process32First()
y Process32Next()
.
En el paso dos, una vez enumerado el PID, debemos abrir el proceso. Esto se puede lograr desde una variedad de llamadas a la API de Windows: GetModuleHandle
, GetProcAddress
o OpenProcess
.
En el paso tres, se debe asignar memoria para que resida la DLL maliciosa proporcionada. Como ocurre con la mayoría de los inyectores, esto se puede lograr usando VirtualAllocEx
.
En el paso cuatro, debemos escribir la DLL maliciosa en la ubicación de memoria asignada. Podemos usar WriteProcessMemory
para escribir en la región asignada.
En el paso cinco, nuestra DLL maliciosa se escribe en la memoria y todo lo que tenemos que hacer es cargarla y ejecutarla. Para cargar la DLL necesitamos usar LoadLibrary
; importado de kernel32
. Una vez cargado, CreateRemoteThread
se puede utilizar para ejecutar la memoria utilizando LoadLibrary
como función inicial.
Podemos compilar estos pasos juntos para crear un inyector DLL. Utilice el inyector de C++ proporcionado y experimente con la inyección de DLL.
Dependiendo del entorno en el que se encuentre, es posible que necesite modificar la forma en que ejecuta su código shell. Esto podría ocurrir cuando hay ganchos en una llamada API y no puede evadirlos o desengancharlos, un EDR está monitoreando subprocesos, etc.
Hasta este punto, hemos analizado principalmente métodos de asignación y escritura de datos hacia y desde procesos locales/remotos. La ejecución es también un paso vital en cualquier técnica de inyección; aunque no es tan importante cuando se intenta minimizar los artefactos de la memoria y los IOC ( indicadores de compromiso ). A diferencia de la asignación y escritura de datos, la ejecución tiene muchas opciones para elegir.
A lo largo de esta sala, hemos observado la ejecución principalmente a través de CreateThread
y su contraparte, CreateRemoteThread
.
En esta tarea cubriremos otros tres métodos de ejecución que se pueden utilizar según las circunstancias de su entorno.
El puntero de función nula es un método extrañamente novedoso de ejecución de bloques de memoria que se basa únicamente en el encasillamiento.
Esta técnica solo se puede ejecutar con memoria asignada localmente, pero no depende de ninguna llamada API ni de ninguna otra funcionalidad del sistema.
El siguiente resumen es la forma más común del puntero de función nula, pero podemos desglosarlo más para explicar sus componentes.
Esta frase breve puede ser difícil de comprender o explicar porque es muy densa. Repasémosla mientras procesa el puntero.
Crear un puntero de función (void(*)()
, delineado en rojo
Transmita el puntero de memoria asignado o la matriz de código shell al puntero de función (<function pointer>)addressPointer)
, delineado en amarillo
Invoca el puntero de función para ejecutar el código shell ();
, resaltado en verde
Esta técnica tiene un caso de uso muy específico, pero puede resultar muy evasiva y útil cuando sea necesario.
Una función APC está en cola en un hilo a través de QueueUserAPC
. Una vez en cola, la función APC produce una interrupción del software y ejecuta la función la próxima vez que se programa el hilo.
Para que una aplicación de usuario/modo de usuario ponga en cola una función APC, el subproceso debe estar en un " estado de alerta ". Un estado de alerta requiere que el hilo esté esperando una devolución de llamada como WaitForSingleObject
o Sleep
.
Ahora que entendemos qué son las funciones de APC, veamos cómo se pueden utilizar de forma maliciosa. Usaremos VirtualAllocEx
y WriteProcessMemory
para asignar y escribir en la memoria.
Esta técnica es una excelente alternativa a la ejecución de subprocesos, pero recientemente ha ganado terreno en la ingeniería de detección y se están implementando trampas específicas para el abuso de APC. Esta puede seguir siendo una gran opción dependiendo de las medidas de detección a las que se enfrente.
Una técnica comúnmente vista en la investigación de malware es PE ( P ortable Executable ) y manipulación de secciones. Como repaso, el formato PE define la estructura y el formato de un archivo ejecutable en Windows. Para fines de ejecución, nos centramos principalmente en las secciones, específicamente .data
y .text
, las tablas y los punteros a las secciones también se usan comúnmente para ejecutar datos.
No profundizaremos en estas técnicas ya que son complejas y requieren un gran desglose técnico, pero sí comentaremos sus principios básicos.
Para comenzar con cualquier técnica de manipulación de secciones, necesitamos obtener un volcado de PE. La obtención de un volcado de PE se logra comúnmente con una DLL u otro archivo malicioso introducido en el archivo xxd
.
En el centro de cada método, se utilizan las matemáticas para moverse a través de los datos hexadecimales físicos que se traducen a datos PE.
Algunas de las técnicas más conocidas incluyen el análisis de puntos de entrada RVA, el mapeo de secciones y el análisis de tablas de reubicación.
Para comprender las implicaciones de la inyección de procesos, podemos observar los TTP ( tácticas , técnicas y procedimientos ) de TrickBot.
TrickBot es un conocido malware bancario que recientemente ha recuperado popularidad en el crimeware financiero. La función principal del malware que observaremos es la de enganchar al navegador. El enlace del navegador permite que el malware conecte llamadas API interesantes que pueden usarse para interceptar/robar credenciales.
Para comenzar nuestro análisis, veamos cómo se dirigen a los navegadores. A partir de la ingeniería inversa de SentinelLab , queda claro que OpenProcess
se está utilizando para obtener identificadores de rutas de navegador comunes; visto en el desmontaje a continuación.
El código fuente actual para la inyección reflectante no está claro, pero SentinelLabs ha descrito el flujo básico del programa de la inyección a continuación.
Proceso de destino abierto,OpenProcess
Asignar memoria,VirtualAllocEx
Copie la función en la memoria asignada,WriteProcessMemory
Copie el código shell en la memoria asignada,WriteProcessMemory
Vaciar el caché para confirmar los cambios.FlushInstructionCache
Crea un hilo remoto,RemoteThread
Reanudar el hilo o recurrir a él para crear un nuevo hilo de usuario, ResumeThread
oRtlCreateUserThread
Una vez inyectado, TrickBot llamará a su función de instalación de gancho copiada en la memoria en el paso tres. SentinelLabs proporciona el pseudocódigo para la función del instalador a continuación.
Analicemos este código, puede parecer desalentador al principio, pero se puede dividir en secciones más pequeñas del conocimiento que hemos adquirido a lo largo de esta sala.
La primera sección de código interesante que vemos se puede identificar como punteros de función; Quizás recuerdes esto de la tarea anterior sobre cómo invocar punteros de función.
Una vez definidos los punteros de función, el malware los utilizará para modificar las protecciones de memoria de la función mediante VirtualProtectEx
.
En este punto, el código se convierte en un negocio divertido de malware con enlace de puntero de función. No es esencial comprender los requisitos técnicos de este código para esta sala. Básicamente, esta sección de código reescribirá un gancho para señalar un salto de código de operación.
Una vez enganchado, devolverá la función a sus protecciones de memoria originales.
De la sobre llamadas a procedimientos asincrónicos, "Una llamada a procedimiento asincrónico (APC) es una función que se ejecuta de forma asincrónica en el contexto de un subproceso en particular".
Crédito por la investigación inicial: