Inyección de proceso de Windows: escribir la carga útil

Introducción

El propósito de esta publicación es discutir un Código Independiente de Posición (PIC) escrito en C que se utilizará para demostrar la inyección de procesos en el sistema operativo Windows. La carga útil simplemente ejecutará una instancia de la calculadora, y no pretende ser maliciosa de ninguna manera. En las publicaciones de seguimiento, analizaré varios métodos de inyección para ayudar al lector a comprender cómo funcionan. A continuación se muestra una captura de pantalla del método PROPagate en acción, que espero discutir en una publicación de seguimiento.

Prototipos de funciones

La mayoría de los métodos de inyección requieren un PIC para ejecutarse con éxito en un espacio de proceso remoto, a menos que, por supuesto, uno quiera cargar una biblioteca de enlace dinámico. (DLL) En el caso de cargar una DLL, solo se necesita ejecutar la API LoadLibrary que proporciona la ruta de una DLL como parámetro. Tradicionalmente, los PIC se han escrito en C o ensamblado, pero existen problemas al usar código de ensamblado puro. Tenga en cuenta que Windows puede ejecutarse en múltiples arquitecturas (por ejemplo, x86, amd64, arm), con múltiples convenciones de llamadas (por ejemplo, stdcall, fastcall). ¿Qué sucede si se requieren correcciones o cambios en el código? Por esas razones, tiene más sentido usar un lenguaje de alto nivel (HLL) como C.
Algunos de los métodos que se discutirán tienen requisitos individuales. Algunas de las funciones de devolución de llamada ejecutadas pasarán una serie de parámetros, y debido a la convención de llamada esperarán que esos parámetros se eliminen al regresar a la persona que llama.

Procedimiento de hilo

La creación de un nuevo hilo en un proceso remoto se puede realizar utilizando una de las siguientes API.
  • CreateRemoteThread
  • RtlCreateUserThread
  • NtCreateThreadEx
  • ZwCreateThreadEx
Cada API espera un puntero a una función de devolución de llamada ThreadProc. Lo siguiente se define en el SDK de Windows. Esto también es perfecto para LoadLibrary que solo espera un parámetro.
DWORD  WINAPI ThreadProc ( 
  _In_ LPVOID lpParameter ) ;

Llamada a procedimiento asincrónico

También es posible adjuntar un PIC a un hilo existente utilizando una de las siguientes API. El hilo debe ser alertable para que esto funcione, y no hay una manera conveniente de determinar si un hilo es alertable o no.
  • QueueUserAPC
  • NtQueueApcThread
  • NtQueueApcThreadEx
  • ZwQueueApcThread
  • ZwQueueApcThreadEx
  • RtlQueueApcWow64Thread
VOID  CALLBACK APCProc ( 
  _IN_ ULONG_PTR dwParam ) ;

Función de devolución de llamada de WindowProc

¿Algunos de ustedes sabrán del método de inyección de memoria extra de ventana (EWM)? El método bien conocido implica reemplazar el objeto "CTray" que se usa para controlar el comportamiento de la clase "Shell_TrayWnd" registrada por explorer.exe. Algunas versiones de Windows permiten actualizar este objeto a través de SetWindowLongPtr, por lo tanto, permite la ejecución de código sin la creación de un nuevo hilo, pero más sobre eso más adelante.
La siguiente API se puede utilizar para actualizar la función de devolución de llamada de la ventana.
  • SetWindowLong (32 bits)
  • GetWindowLong (32 bits)
  • SetWindowLongPtr (32 y 64 bits)
  • GetWindowLongPtr (32 y 64 bits)
  • SetClassLongPtr (32 y 64 bits)
  • GetClassLongPtr (32 y 64 bits)
LRESULT  CALLBACK  WindowProc ( 
  _In_ HWND    hwnd , 
  _In_ UINT    uMsg , 
  _In_ WPARAM wParam , 
  _In_ LPARAM lParam ) ;

Función de devolución de llamada de subclase

El método PROPagate lleva el nombre de las API GetProp y SetProp utilizadas para cambiar la dirección de una función de devolución de llamada para una ventana subclasificada. A partir de julio de 2018, esta es una técnica relativamente nueva que es similar a la inyección EWM o los ataques de ruptura descritos en 2002.
La siguiente API (pero no todas) puede ser de interés.
  • GetProp
  • SetProp
  • EnumProps
typedef  LRESULT  (  CALLBACK  * SUBCLASSPROC ) ( 
   HWND       hWnd , 
   UINT       uMsg , 
   WPARAM     wParam , 
   LPARAM     lParam , 
   UINT_PTR   uIdSubclass , 
   DWORD_PTR dwRefData ) ;

Biblioteca de enlace dinámico (DLL)

Aunque no es un PIC, la inyección de proceso a veces puede implicar cargar una DLL. Compilar la carga útil en un archivo DLL es, por lo tanto, una opción.
__declspec ( dllexport ) 
BOOL  WINAPI DllMain ( HINSTANCE hInstance ,  
  DWORD fdwReason ,  LPVOID lpvReserved ) ;

Compilando

Para generar la carga útil adecuada utilizando los parámetros correctos y el tipo de retorno, utilizamos la directiva define. Si necesitamos compilar la carga útil para otro prototipo de función, sería relativamente fácil modificar este código.
# Ifdef XAPC // Procedimiento asíncrona llamada 
VOID  RETROLLAMADA APCProc ( ULONG_PTR dwParam ) 
# endif

# Ifdef HILO // Crear remota hilo 
DWORD  WINAPI ThreadProc ( LPVOID lpParameter ) 
# endif

# Ifdef SUBCLASE // Ventana subclase procedimiento de devolución de llamada 
LRESULT  CALLBACK SubclassProc ( HWND hWnd ,  UINT uMsg ,  WPARAM wParam ,  
  LPARAM lParam ,  UINT_PTR uIdSubclass ,  DWORD_PTR dwRefData ) 
# endif

# ifdef WINDOW // Procedimiento de devolución de llamada de ventana 
LRESULT  CALLBACK WndProc ( HWND hWnd ,  UINT uMsg ,  
  WPARAM wParam ,  LPARAM lParam ) 
# endif

# ifdef DLL     // compilar como Dynamic-link Library 
# advertencia pragma (push) 
# advertencia pragma (deshabilitar: 4100) 
__declspec ( dllexport ) 
BOOL  WINAPI DllMain ( HINSTANCE hInstance ,  
  DWORD fdwReason ,  LPVOID lpvReserved ) 
# endif
¿Qué pasa si la carga útil se llama varias veces? Para cada método discutido, abordaré esa pregunta.

Declarando cuerdas

Si declara una cadena en C y compila la fuente en un ejecutable, puede encontrar literales de cadena almacenados en una sección de memoria de solo lectura. Este es un problema para un PIC porque necesitamos todas las variables, incluidas las cadenas almacenadas en la pila. Para ilustrar el problema, considere el siguiente fragmento de código simple.
# include < stdio.h >

int  main ( void ) { 
    char msg [ ] = " ¡Hola, mundo! \ n " ;

    printf ( " % s " , mensaje ) ; 
    devuelve  0 ; 
}
Aunque esta salida de ensamblaje del compilador MSVC no es muy legible, muestra que la pila no se usa para el almacenamiento local del literal de cadena.
La solución podría ser declarar la cadena como una matriz de bytes, como la siguiente.
# include < stdio.h >

int  main ( void ) { 
    char msg [ ] = { 'H' , 'e' , 'l' , 'l' , 'o' , ',' , '' , 'W' , 'o' , 'r' , 'l' , 'd' , '!' , '\ n' , 0 } ;

    printf ( " % s " , mensaje ) ; 
    devuelve  0 ; 
}
El resultado del ensamblaje de esto muestra que MSVC usará la pila local.
Esto funciona para MSVC, pero desafortunadamente no para GCC que aún almacenará la cadena en la sección de solo lectura del ejecutable. ¿Qué otras opciones hay?

Usar ensamblaje en línea

Si no recuerdo mal, fue Z0MBiE / 29a quien una vez sugirió en Solucionar problemas de cadenas simples en HLL usando un ensamblaje basado en macro con el compilador Borland C para almacenar cadenas en la pila con el fin de ofuscarlas. En Phrack # 69, Shellcode de la mejor manera, o cómo usar su compilador por fishstiqz sugiere usar una macro en línea.
# define INLINE_STR ( name , str ) \ 
        const char * name ;           \ 
        asm (                         \ 
" call 1f \ n "              \ " .asciz \" " str " \ " \ n "   \ " 1: \ n "                   \ " pop% 0 \ n "               \ : " = r "            
            
            
            
                         \ 
) ;
La macro se puede usar con lo siguiente.
INLINE_STR ( kernel32 ,  " kernel32 " ) ; 
PVOID pKernel32 = scGetModuleBase ( kernel32 ) ;
Dependemos del código de ensamblaje aquí, y eso es precisamente lo que debemos evitar. Tampoco podemos ensamblar en línea los 64 bits, como señala fishstiqz. ¿Qué más?

Declaración utilizando matrices

Después de examinar varias opciones y de haber seguido el consejo de otros (@solardiz) declarando cadenas como matrices de 32 bits, se resuelve el problema tanto para GCC como para MSVC.
# include < stdio.h >

typedef  unsigned  int W ;

int  main ( void ) { 
    W msg [ 4 ] ;
    
    msg [ 0 ] = * ( W * ) " Infierno " ; 
    msg [ 1 ] = * ( W * ) " o, W " ; 
    msg [ 2 ] = * ( W * ) " orld " ; 
    msg [ 3 ] = * ( W * ) " ! \ n " ;

    printf ( " % s " ,  ( char * ) msg ) ; 
    devuelve  0 ; 
}
Como puede ver en la salida de MSVC y GCC, ambos colocan la cadena en la pila.
Salida MSVC
Salida de GCC 
Por supuesto, tendría sentido automatizar este proceso, en lugar de intentar inicializar manualmente 😉

Declaración de punteros de función

No tiene que declarar punteros de función de esta manera, pero para un shellcode, hace que el código sea mucho más fácil de leer.
typedef  HANDLE  ( WINAPI  * OpenEvent_t ) ( 
  _In_ DWORD    dwDesiredAccess , 
  _In_ BOOL     bInheritHandle , 
  _In_ LPCTSTR lpName ) ;

typedef  BOOL  ( WINAPI  * SetEvent_t ) ( 
  _In_ HANDLE hEvent ) ;
  
typedef  BOOL  ( WINAPI  * CloseHandle_t ) ( 
  _In_ HANDLE hOject ) ;
  
typedef  UINT  ( WINAPI  * WinExec_t ) ( 
  _In_ LPCSTR lpCmdLine , _In_ UINT uCmdShow ) ;
Una vez que se define la función, puede declararla dentro del código principal, así.
SetEvent_t pSetEvent ; 
OpenEvent_t pOpenEvent ; 
CloseHandle_t pCloseHandle ; 
WinExec_t pWinExec ;

Resolviendo direcciones API

En lugar de cubrir el mismo terreno, recomiendo leer dos publicaciones sobre esta tarea. Resolviendo direcciones API en memoria y Fido, cómo resuelve GetProcAddress y LoadLibraryA . Ambos cubren algunas buenas formas de resolver API dinámicamente utilizando la tabla de direcciones de importación (IAT) y la tabla de direcciones de exportación (EAT) de un archivo ejecutable portátil (PE).
Atravesar los módulos del Bloque de entorno de proceso (PEB) se encuentra comúnmente en los códigos de shell, y se requiere que un PIC resuelva la dirección de las funciones API.
// busca en todos los módulos del PEB API 
LPVOID xGetProcAddress ( LPVOID pszAPI )  { 
    PPEB peb ; 
    PPEB_LDR_DATA ldr ; 
    PLDR_DATA_TABLE_ENTRY dte ; 
    LPVOID                 api_adr = NULL ;
    
  # si está definido ( _WIN64 ) 
    peb = ( PPEB ) __readgsqword ( 0x60 ) ; # else 
    peb = ( PPEB ) __readfsdword ( 0x30 ) ; # endif  
   
  

    ldr =  ( PPEB_LDR_DATA ) peb - > Ldr ;
    
    // para cada DLL cargado 
    DTE = ( PLDR_DATA_TABLE_ENTRY ) LDR - > InLoadOrderModuleList . Flink ; 
    para  ( ; DTE - > DllBase ! =  NULL  y Y api_adr = =  NULL ;  
         DTE = ( PLDR_DATA_TABLE_ENTRY ) DTE - > InLoadOrderLinks . Flink ) 
    { 
      // buscar en la tabla de exportación de API 
      api_adr =FindExport ( DTE - > DllBase ,  ( PChar ) pszAPI ) ;   
    } 
    return api_adr ; 
}

Buscar en la tabla de direcciones de exportación (EAT)

Ciertamente, se puede buscar en el IAT la dirección de la API, pero a continuación se utiliza el EAT. Esta función no maneja referencias hacia adelante y también estamos buscando por cadena, en lugar de hash, que normalmente se usa en shellcode.
// localizar la dirección de API en la tabla de direcciones de exportación 
LPVOID FindExport ( LPVOID de base ,  PChar pszAPI ) { 
    dos PIMAGE_DOS_HEADER ; 
    PIMAGE_NT_HEADERS nt ; 
    DWORD                    cnt , rva , dll_h ; 
    Directorio PIMAGE_DATA_DIRECTORY ; 
    PIMAGE_EXPORT_DIRECTORY exp ; 
    PDWORD                   adr ; 
    PDWORD                   sym ; 
    PWORD                    ord ; 
    PCHAR                   api , dll ; 
    LPVOID                   api_adr = NULL ;
    
    dos =  ( PIMAGE_DOS_HEADER ) base ; 
    nt   = RVA2VA ( PIMAGE_NT_HEADERS , base , dos - > e_lfanew ) ; 
    dir =  ( PIMAGE_DATA_DIRECTORY ) nt - > OpcionalHeader . DataDirectory ; 
    rva = dir [ IMAGE_DIRECTORY_ENTRY_EXPORT ] . VirtualAddress ;
    
    // si no hay tabla de exportación, devuelve NULL 
    if  ( rva = = 0 )  return  NULL ;
    
    exp  =  ( PIMAGE_EXPORT_DIRECTORY ) RVA2VA ( ULONG_PTR , base , rva ) ; 
    cnt =  exp - > NumberOfNames ;
    
    // si no hay nombres de API, devuelve NULL 
    if  ( cnt = = 0 )  return  NULL ;
    
    adr = RVA2VA ( PDWORD , base ,  exp - > AddressOfFunctions ) ; 
    sym = RVA2VA ( PDWORD , base ,  exp - > AddressOfNames ) ; 
    ord = RVA2VA ( PWORD , base ,  exp - > AddressOfNameOrdinals ) ; 
    dll = RVA2VA ( PCHAR , base ,  exp- > Nombre ) ;
    
    do  { 
      // calcular el hash de la cadena 
      api api = RVA2VA ( PCHAR , base , sym [ cnt - 1 ] ) ; 
      // agregar al hash de DLL y comparar 
      if  ( ! xstrcmp ( pszAPI , api ) ) { 
        // dirección de retorno de la función 
        api_adr = RVA2VA ( LPVOID , base , adr [ ord [ cnt - 1 ] ]) ; 
        return api_adr ; 
      } 
    }  while  ( - - cnt & & api_adr = = 0 ) ; 
    return api_adr ; 
}

Resumen

Si usamos C en lugar de ensamblar, escribir una carga útil no es tan difícil. Las próximas publicaciones sobre este tema cubrirán algunos métodos de inyección individualmente.
Carga útil para sistemas de 32 y 64 bits que ejecutan el bloc de notas. El bloc de notas tuvo que usarse en lugar de la calculadora porque algunos procesos host no admiten aplicaciones de metro

Comentarios