En programación de computadoras , la inyección de DLL es una técnica utilizada para ejecutar código dentro del espacio de direcciones de otro proceso obligándolo a cargar una biblioteca de enlaces dinámicos . [1] Los programas externos suelen utilizar la inyección de DLL para influir en el comportamiento de otro programa de una manera que sus autores no anticiparon ni pretendieron. [1] [2] [3] Por ejemplo, el código inyectado podría conectar llamadas a funciones del sistema, [4] [5] o leer el contenido de los cuadros de texto de contraseñas , lo que no se puede hacer de la forma habitual. [6] Un programa utilizado para inyectar código arbitrario en procesos arbitrarios se llama inyector DLL .
Hay varias formas en Microsoft Windows de forzar que un proceso cargue y ejecute código en una DLL que los autores no pretendían:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs
se cargan en cada proceso que carga User32.dll durante la llamada inicial de esa DLL. [7] [8] [9] A partir de Windows Vista , los AppInit_DLL están deshabilitados de forma predeterminada. [10] A partir de Windows 7, la infraestructura AppInit_DLL admite la firma de código . A partir de Windows 8 , toda la funcionalidad AppInit_DLL está deshabilitada cuando el arranque seguro está habilitado, independientemente de la firma del código o la configuración del registro. [11]HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\AppCertDLLs
se cargan en cada proceso que llama a las funciones de la API de Win32CreateProcess
, CreateProcessAsUser
, CreateProcessWithLogonW
y . Esa es la forma correcta de utilizar la inyección legal de DLL en la versión actual de Windows: Windows 10. La DLL debe estar firmada con un certificado válido.CreateProcessWithTokenW
WinExec
CreateRemoteThread
las técnicas de inyección de código , como AtomBombing, [12] se pueden utilizar para inyectar una DLL en un programa después de que se haya iniciado. [5] [6] [13] [14] [15] [16]32.dll
, sería posible cargar una biblioteca llamada 32.dll
[ cita requerida ] . En el pasado se ha demostrado que esta técnica es eficaz contra un método de protección de procesos contra la inyección de DLL. [24]LoadLibrary
y el argumento establecido como la dirección de la cadena que acaba de cargar en el destino. [13] [26]LoadLibrary
, se puede escribir el código que se ejecutará en el destino e iniciar el hilo en ese código. [6]DLL_THREAD_ATTACH
notificaciones enviadas a cada módulo cargado cuando se inicia un hilo. [27]SetWindowsHookEx
. [2] [5] [6] [28] [29] [30]SuspendThread
o NtSuspendThread
para suspender todos los subprocesos y luego utilice la función SetThreadContext
o NtSetContextThread
para modificar el contexto de un subproceso existente en la aplicación para ejecutar código inyectado, que a su vez podría cargar una DLL. [4] [31] [32]LoadLibrary
o LoadLibraryEx
sin especificar una ruta completa a la DLL que se está cargando. [33] [34] [35]En sistemas operativos tipo Unix con el enlazador dinámico basado en ld.so (en BSD ) y ld-linux.so (en Linux ), se pueden vincular bibliotecas arbitrarias a un nuevo proceso ingresando el nombre de ruta de la biblioteca en la variable de entorno LD_PRELOAD . que se pueden configurar globalmente o individualmente para un solo proceso. [37]
Por ejemplo, en un sistema Linux, este comando inicia el comando "prog" con la biblioteca compartida del archivo "test.so" vinculada en el momento del lanzamiento:
LD_PRELOAD = programa "./test.so"
Una biblioteca de este tipo se puede crear de la misma manera que otros objetos compartidos . Con GCC , esto implica compilar el archivo fuente que contiene los nuevos globales a vincular, con la opción -fpic o -fPIC , [38] y vincularlo con la opción -shared . [39] La biblioteca tiene acceso a los símbolos externos declarados en el programa como cualquier otra biblioteca.
En macOS , el siguiente comando inicia el comando "prog" con la biblioteca compartida del archivo "test.dylib" vinculada en el momento del lanzamiento: [40]
DYLD_INSERT_LIBRARIES = "./test.dylib" DYLD_FORCE_FLAT_NAMESPACE = 1 programa
También es posible utilizar técnicas basadas en depuradores en sistemas tipo Unix. [41]
Como no hay ninguna LoadLibrary()
llamada para cargar una DLL en un proceso externo, debe copiar una DLL cargada localmente en la memoria asignada de forma remota. El siguiente código comentado muestra cómo hacerlo.
#include <Windows.h> #include <TlHelp32.h> #include <iostream> #include <memoria> #include <system_error> #include <charconv> #include <vector> #include <cassert> #si está definido (_MSC_VER) #advertencia pragma (deshabilitar: 6387) #endifusando el espacio de nombres estándar ; usando XHANDLE = Unique_ptr < void , decltype ([]( void * h ) { h && h != INVALID_HANDLE_VALUE && CloseHandle ( ( HANDLE ) h ); }) > ; usando XHMODULE = Unique_ptr < remove_reference_t < decltype ( * HMODULE ()) > , decltype ([]( HMODULE hm ) { hm && FreeLibrary ( hm ); }) > ; MODULEENTRY32W getModuleDescription ( HMODULE hmModule ); size_t maxReadableRange ( vacío * pRegión ); cadena getAbsolutePathA ( char const * fileName , char const * err ); DWORD dumpParseDWORD ( wchar_t const * str ); wstring getAbsolutePath ( wchar_t const * makeAbsolute , char const * errStr ); [[ noreturn ]] void throwSysErr ( char const * str ); constexpr wchar_t const * LOADER_DLL_NAME = L "loaderDll.dll" ; constexpr char const * LOADER_THREAD_PROC = "loadLibraryThread" ; int wmain ( int argc , wchar_t ** argv ) { intenta { si ( argc < 3 ) devuelve EXIT_FAILURE ; wchar_t const * ProcessId = argv [ 1 ], * remotoLoadedDll = argv [ 2 ], * initData = argc >= 4 ? argv [ 3 ] : L "" ; DWORD dwProcessId = tontoParseDWORD ( id de proceso ); XHANDLE xhProcess ( OpenProcess ( PROCESS_ALL_ACCESS , FALSE , dwProcessId ) ); if ( ! xhProcess . get () ) throwSysErr ( "no se puede abrir el proceso remoto con acceso ilimitado" ); XHMODULE xhmLocalLoader ; ENTRADA DE MÓDULO32W meLocalLoader ; para ( ; ; ) { xhmLocalLoader . restablecer ( LoadLibraryW ( LOADER_DLL_NAME ) ); if ( ! xhmLocalLoader . get () ) throwSysErr ( "no se puede cargar la DLL del cargador localmente" ); // obtener la dirección inicial y el tamaño del módulo meLocalLoader = getModuleDescription ( ( HMODULE ) xhmLocalLoader . get () ); // intenta asignar un rango de memoria en el proceso externo con el mismo tamaño que ocupa la DLL en nuestro proceso if ( VirtualAllocEx ( xhProcess . get (), meLocalLoader . modBaseAddr , meLocalLoader . modBaseSize , MEM_RESERVE | MEM_COMMIT , PAGE_EXECUTE_READWRITE ) ) break ; // la asignación falló, biblioteca gratuita xhmLocalLoader . restablecer ( nulptr ); // intenta reservar el rango de direcciones que la biblioteca ocupaba antes para evitar // el reciclaje de ese rango de direcciones con la siguiente llamada a LoadLibrary(). if ( ! VirtualAlloc ( meLocalLoader . modBaseAddr , meLocalLoader . modBaseSize , MEM_RESERVE , PAGE_NOACCESS ) ) throwSysErr ( "no se puede reservar el rango de direcciones de la DLL previamente asignada" ); } LPTHREAD_START_ROUTINE loaderThreadProc = ( LPTHREAD_START_ROUTINE ) GetProcAddress ( ( HMODULE ) xhmLocalLoader . get (), :: LOADER_THREAD_PROC ); if ( ! loaderThreadProc ) throwSysErr ( "no se puede obtener el punto de entrada del procedimiento" ); // copiar todo el contenido DLL legible al proceso de destino if ( Size_T copiado ; ! WriteProcessMemory ( xhProcess . get (), meLocalLoader . modBaseAddr , meLocalLoader . modBaseAddr , meLocalLoader . modBaseSize , & copiado ) && GetLastError () != ERROR_PARTIAL_COPY ) throwSysErr ( "no se puede copiar la DLL del cargador al proceso remoto" ); // crea dos cadenas C concatenadas que contienen la DLL a cargar así como el parámetro // dado a la DLL cargada de forma remota wstring data ( getAbsolutePath ( remoteLoadedDll , "no se puede obtener la ruta absoluta a la DLL para cargarla de forma remota" ) ); datos += L '\0' ; datos += datosinit ; datos += L '\0' ; tamaño_t tamaño de datos = datos . tamaño () * tamaño de ( wchar_t ); auto initStrErr = []() { tirarSysErr ( "no se pudieron copiar los datos de inicialización a la DLL del cargador" ); }; void * datos remotos ; // asigna de forma remota memoria lo suficientemente grande como para contener al menos nuestras dos cadenas if ( ! ( remoteData = VirtualAllocEx ( xhProcess . get (), nullptr , dataSize , MEM_RESERVE | MEM_COMMIT , PAGE_READWRITE )) ) initStrErr (); // escribe ambas cadenas en la memoria remota if ( TAMAÑO_T copiado ;! WriteProcessMemory ( xhProcess . get (), datos remotos , datos . datos (), tamaño de datos , & copiado ) || copiado ! = tamaño de datos ) initStrErr (); // crear un hilo de carga de DLL remoto; el punto de entrada dado tiene la misma dirección en nuestro proceso así como la dirección remota // le damos a este hilo la dirección de nuestras dos cadenas copiadas remotamente XHANDLE xhRemoteInitThread ( CreateRemoteThread ( xhProcess . get (), nullptr , 0 , loaderThreadProc , remoteData , 0 , nullptr ) ); if ( ! xhRemoteInitThread . get () ) throwSysErr ( "no se pudo crear el hilo de inicialización remota" ); // esperar a que termine el hilo de nuestro cargador remoto // debería hacerlo muy pronto, ya que su única tarea es copiar las cadenas de la DLL cargada de forma remota y cargar esta DLL por sí misma if ( WaitForSingleObject ( xhRemoteInitThread . get (), INFINITE ) == WAIT_FAILED ) throwSysErr ( "no puedo esperar al hilo de inicialización remota" ); DWORD dwInitThreadExitCode ; if ( ! GetExitCodeThread ( xhRemoteInitThread . get (), & dwInitThreadExitCode ) ) throwSysErr ( "no se puede obtener el código de éxito del hilo de inicialización" ); // comprueba el código de salida del cargador remoto, debería ser NO_ERROR (0) if ( dwInitThreadExitCode != NO_ERROR ) throw system_error ( ( int ) dwInitThreadExitCode , system_category (), "Error LoadLibrary() en dll del cargador remoto" ); } catch ( excepción constante y se ) { cout << se . qué () << endl ; } } MODULEENTRY32W getModuleDescription ( HMODULE hmModule ) { // devuelve la ruta absoluta para un identificador de módulo determinado auto getModulePath = []( HMODULE hm , char const * err ) -> wstring { wchar_t modulePath [ MAX_PATH ]; if ( DWORD dwRet = GetModuleFileNameW ( hm , modulePath , MAX_PATH ); ! dwRet || dwRet >= MAX_PATH ) throwSysErr ( err ); devolver ruta del módulo ; }; // ruta del módulo de la DLL local wstring moduleAbsolute ( getModulePath ( hmModule , "no se puede obtener la ruta absoluta para la DLL del cargador local" ) ); XHANDLE xhToolHelp ( CreateToolhelp32Snapshot ( TH32CS_SNAPMODULE , GetCurrentProcessId () ) ); auto toolHelpErr = []() { throwSysErr ( "no se pueden enumerar los módulos en el proceso de inyección" ); }; if ( xhToolHelp . get () == INVALID_HANDLE_VALUE ) toolHelpErr (); MODULEENTRY32W yo ; a mí . dwSize = tamaño de mí ; if ( ! Module32FirstW ( xhToolHelp . get (), & yo ) ) toolHelpErr (); for ( ; ; ) { // tiene la imagen actual en la instantánea la misma ruta que la DLL proporcionada por el identificador del módulo // no es necesario comparar sin distinguir entre mayúsculas y minúsculas porque obtuvimos ambas rutas del kernel para que coincidan exactamente if ( getModulePath ( yo . hModule , "no puedo obtener la ruta absoluta para el nombre de DLL enumerado en la ayuda de herramientas " ) == moduleAbsolute ) devuélveme ; a mí . dwSize = tamaño de mí ; if ( ! Module32NextW ( xhToolHelp . get (), & me ) ) toolHelpErr (); } } [[ noreturn ]] void throwSysErr ( char const * str ) { throw system_error ( ( int ) GetLastError (), system_category (), str ); } DWORD dumpParseDWORD ( wchar_t const * str ) { // from_chars del idiota porque no hay from_chars para caracteres Unicode DWORD dwRet = 0 ; mientras ( * str ) dwRet = dwRet * 10 + ( carácter sin firmar )( * str ++ - L '0' ); devolver dwRet ; } wstring getAbsolutePath ( wchar_t const * makeAbsolute , char const * errStr ) { // obtiene la ruta absoluta de una ruta relativa determinada wstring path ( MAX_PATH , L '\0' ); DWORD dwLongitud ; if ( ! ( dwLength = GetFullPathNameW ( makeAbsolute , MAX_PATH , ruta . data (), nullptr )) ) throwSysErr ( erStr ); // si deRet == MAX_PATH podríamos omitir un carácter de terminación cero, trátelo como un error; de lo contrario, if ( dwLength >= MAX_PATH ) throw invalid_argument ( errStr ); camino . cambiar el tamaño ( dwLength ); vía de retorno ; }
El principal problema resuelto aquí es que una DLL cargada localmente y copiada a un proceso remoto debe ocupar las mismas direcciones que en el proceso de inyección. El código anterior hace esto asignando memoria para el mismo rango de direcciones que el ocupado anteriormente en el proceso de inyección. Si esto falla, la DLL se libera localmente, el rango de direcciones anterior se marca como reservado y LoadLibrary()
se vuelve a intentar la llamada. Al reservar el rango de direcciones anterior, el código evita que el siguiente LoadLibrary()
intento asigne el mismo rango de direcciones utilizado anteriormente.
El principal inconveniente de ese enfoque es que la DLL copiada en el proceso externo es que no hay otras dependencias de la biblioteca DLL de esa DLL cargada en el espacio de direcciones externas o punteros, como llamadas a funciones, a las DLL cargadas por el proceso externo. ajustado de acuerdo con las dependencias de la DLL copiada. Afortunadamente, las DLL generalmente tienen direcciones de carga preferidas que son respetadas por el cargador del kernel . Algunas DLL como kernel32.dll
se cargan de manera confiable al principio, cuando el espacio de direcciones del proceso está ocupado por la imagen ejecutable y las DLL dependientes. Normalmente tienen direcciones fiables y no conflictivas. Por lo tanto, la DLL copiada puede utilizar cualquier kernel32.dll
llamada, por ejemplo, para cargar otra DLL con todas las ventajas de una DLL cargada localmente, es decir, que tenga todas las dependencias relativas de biblioteca. La ruta a esa DLL se copia en el espacio de direcciones externas y se proporciona como un parámetro nulo para la función de subproceso. La implementación anterior también permite tener parámetros adicionales, que se pasan a la DLL copiada de forma remota después de que la cadena con la DLL que se cargará de forma remota se pase a esa DLL.
El siguiente código es la fuente de la DLL del cargador copiada de forma remota que solo realiza kernel32.dll
llamadas:
#incluye <Windows.h> #incluye <atómico> usando el espacio de nombres estándar ; BOOL APIENTRY DllMain ( HMODULE hModule , DWORD ul_reason_for_call , LPVOID lpReserved ) { return TRUE ; } DWORD WINAPI loadLibraryThread ( LPVOID lpvThreadParam ); // MSVC / clang-cl manipulación #if definido(_M_IX86) #pragma comentario(enlazador, "/export:loadLibraryThread=?loadLibraryThread@@YGKPAX@Z") #elif definido(_M_X64) #pragma comentario(enlazador, "/exportar :loadLibraryThread=?loadLibraryThread@@YAKPEAX@Z") #else #error plataforma no compatible #endifDWORD WINAPI loadLibraryThread ( LPVOID lpvThreadParam ) { // uso atomics para evitar que el "optimizador" reemplace mi código con // las llamadas de biblioteca wsclen o memcpy a direcciones externas en realidad no son válidas // con esta DLL copiada // ignora cualquier barrera de carga atómica desde esto no tiene que ser rápido atomic_wchar_t const // ruta a la biblioteca para cargar desde dentro * libPath = ( atomic_wchar_t * ) lpvThreadParam , // puntero a los parámetros dados a esta biblioteca * data = libPath ; // avanza los datos a los parámetros reales while ( * data ++ ); MANEJAR hEventoSaliente ; // crear un evento con nombre para notificar a la DLL remota que los datos ya se han copiado // necesario porque la ejecución remota de la DLL comienza directamente después de LoadLibrary()S if ( ! ( hOutboundEvent = CreateEventA ( nullptr , FALSE , FALSE , "nasty hackers" )) ) devolver GetLastError (); // tamaño de los parámetros dados a la DLL size_t dataSize = 0 ; mientras ( datos [ tamaño de datos ++ ] ); si ( tamaño de datos >= MAX_PATH ) devuelve ERROR_INVALID_PARAMETER ; // limpia LoadLibrary() con todas las dependencias de DLL HMODULE hm = LoadLibraryW ( ( wchar_t * ) libPath ); si ( ! hm ) devuelve GetLastError (); // obtener la dirección de exportación de parámetros de la DLL cargada wchar_t volatile ( & initData )[ MAX_PATH ] = * ( wchar_t ( ) [ MAX_PATH ]) GetProcAddress ( hm , "initData" ); // ¿La DLL cargada no proporciona dicha exportación, es decir, no depende de parámetros? si ( ! initData ) devuelve NO_ERROR ; // copia los parámetros a la DLL para ( size_t i = 0 ; i ! = dataSize ; initData [ i ] = data [ i ], ++ i ); // notifica que los parámetros están disponibles si ( ! SetEvent ( hOutboundEvent ) ) return GetLastError (); devolver NO_ERROR ; }
El último código muestra un ejemplo de una DLL cargada por la DLL del cargador que imprime los parámetros en un archivo.
#incluye <Windows.h> #incluye <fstream> #incluye <atómico> usando el espacio de nombres estándar ; #if definido(_MSC_VER) #pragma advertencia(disable: 6387) // el identificador devuelto podría ser nulo #endif#si está definido (_M_IX86) #comentario de pragma (enlazador, "/export:DllMain=_DllMain@12") #elif definido (_M_X64) #comentario de pragma (enlazador, "/export:DllMain=_DllMain@12") #else #error plataforma no compatible #endifusando el espacio de nombres estándar ; DWORD WINAPI myThread ( LPVOID lpvThreadParam ); BOOL APIENTRY DllMain ( HMODULE hModule , DWORD dwReason , LPVOID lpReserved ) { switch ( dwReason ) { case DLL_PROCESS_ATTACH : // crear hilo ya que no se llama a ninguna exportación desde la DLL del cargador CreateThread ( nullptr , 0 , myThread , nullptr , 0 , nullptr ) ; predeterminado : romper ; } devuelve VERDADERO ; } externo " C" __declspec ( dllexport ) wchar_t initData [ MAX_PATH ] = {0} ; DWORD WINAPI myThread ( LPVOID lpvThreadParam ) { // espera a que initData sea completado por la DLL del cargador // salta eso si no confías en ningún initData // ya que el evento denominado "hackers desagradables" ha sido creado por nuestra propia DLL / / LoadLibrary() solo nos estamos conectando a un evento con nombre, pero no creando uno if ( WaitForSingleObject ( CreateEventA ( nullptr , FALSE , FALSE , "hackers desagradables" ) , INFINITE ) != WAIT_OBJECT_0 ) return 0 ; // escribe parámetros en un archivo para probar la función // el siguiente código no funciona cuando la DLL no está vinculada estáticamente por razones desconocidas wofstream wofs ; ¡guau ! open ( "c: \\ Usuarios \\ xxx \\ test.txt " , ofstream :: out | ofstream :: trunc ); wofs << initData << endl ; devolver 0 ; }
Un hecho importante es que no se realizan exportaciones desde la DLL del cargador, sino que toda la inicialización se realiza desde DllMain
. La única exportación es la de initData
, que recibe los parámetros dados por el proceso de inyección a través de la DLL del cargador. Y hay que tener en cuenta que el subproceso creado a partir de una función DllMain no está programado hasta que su DLL_THREAD_ATTACH
función haya tenido éxito. Por lo tanto, es posible que no haya ninguna sincronización desde adentro DllMain
con el hilo creado.
Genere código independiente de la posición (PIC) adecuado para su uso en una biblioteca compartida, si es compatible con la máquina de destino.
cuadrados.
-fpic
Produzca un objeto compartido que luego pueda vincularse con otros objetos para formar un ejecutable.
cuadrados
-shared