Cargando un dll desde un dll?

¿Cuál es la mejor manera de cargar un dll desde un dll?

Mi problema es que no puedo cargar un dll en process_attach, y no puedo cargar el dll desde el progtwig principal, porque no controlo la fuente principal del progtwig. Y, por lo tanto, tampoco puedo llamar a una función que no sea dllmain.

Después de todo el debate que siguió en los comentarios, creo que es mejor resumir mis posiciones en una respuesta “real”.

En primer lugar, todavía no está claro por qué necesita cargar un dll en DllMain con LoadLibrary. Esto definitivamente es una mala idea, ya que su DllMain se ejecuta dentro de otra llamada a LoadLibrary, que contiene el locking del cargador, como se explica en la documentación de DllMain :

Durante el inicio del proceso inicial o después de una llamada a LoadLibrary, el sistema escanea la lista de archivos DLL cargados para el proceso. Para cada DLL que no se haya invocado con el valor DLL_PROCESS_ATTACH, el sistema llama a la función de punto de entrada de la DLL. Esta llamada se realiza en el contexto del subproceso que provocó el cambio del espacio de direcciones del proceso, como el subproceso principal del proceso o el subproceso que llamó LoadLibrary. El acceso al punto de entrada es serializado por el sistema en todo el proceso. Los subprocesos en DllMain mantienen el locking del cargador para que no se puedan cargar o inicializar dinámicamente DLL adicionales.

La función de punto de entrada debe realizar solo tareas simples de inicialización o terminación . No debe invocar la función LoadLibrary o LoadLibraryEx (o una función que invoca estas funciones) , ya que esto puede crear bucles de dependencia en el orden de carga DLL. Esto puede provocar que se utilice una DLL antes de que el sistema haya ejecutado su código de inicialización. De forma similar, la función de punto de entrada no debe llamar a la función FreeLibrary (o una función que llama a FreeLibrary) durante la terminación del proceso, ya que esto puede dar como resultado que se use una DLL después de que el sistema haya ejecutado su código de terminación.

(énfasis añadido)

Entonces, esto sobre por qué está prohibido; para una explicación más clara y detallada, vea esto y esto , para algunos otros ejemplos sobre lo que puede suceder si no se apega a estas reglas en DllMain vea también algunas publicaciones en el blog de Raymond Chen .

Ahora, en la respuesta de Rakis.

Como ya he repetido varias veces, lo que piensas que es DllMain, no es el DllMain real del dll; en cambio, es solo una función que es llamada por el punto de entrada real de la dll. Éste, a su vez, es tomado automáticamente por el CRT para realizar sus tareas adicionales de inicialización / limpieza, entre las cuales está la construcción de objetos globales y de los campos estáticos de las clases (en realidad todos estos desde la perspectiva del comstackdor son casi los mismos cosa). Después (o antes, para la limpieza) de tales tareas, llama a su DllMain.

De alguna manera va de esta manera (obviamente no escribí toda la lógica de comprobación de errores, solo para mostrar cómo funciona):

/* This is actually the function that the linker marks as entrypoint for the dll */ BOOL WINAPI CRTDllMain( __in HINSTANCE hinstDLL, __in DWORD fdwReason, __in LPVOID lpvReserved ) { BOOL ret=FALSE; switch(fdwReason) { case DLL_PROCESS_ATTACH: /* Init the global CRT structures */ init_CRT(); /* Construct global objects and static fields */ construct_globals(); /* Call user-supplied DllMain and get from it the return code */ ret = DllMain(hinstDLL, fdwReason, lpvReserved); break; case DLL_PROCESS_DETACH: /* Call user-supplied DllMain and get from it the return code */ ret = DllMain(hinstDLL, fdwReason, lpvReserved); /* Destruct global objects and static fields */ destruct_globals(); /* Destruct the global CRT structures */ cleanup_CRT(); break; case DLL_THREAD_ATTACH: /* Init the CRT thread-local structures */ init_TLS_CRT(); /* The same as before, but for thread-local objects */ construct_TLS_globals(); /* Call user-supplied DllMain and get from it the return code */ ret = DllMain(hinstDLL, fdwReason, lpvReserved); break; case DLL_THREAD_DETACH: /* Call user-supplied DllMain and get from it the return code */ ret = DllMain(hinstDLL, fdwReason, lpvReserved); /* Destruct thread-local objects and static fields */ destruct_TLS_globals(); /* Destruct the thread-local CRT structures */ cleanup_TLS_CRT(); break; default: /* ?!? */ /* Call user-supplied DllMain and get from it the return code */ ret = DllMain(hinstDLL, fdwReason, lpvReserved); } return ret; } 

No hay nada de especial en esto: también ocurre con los ejecutables normales, con su principal llamado por el punto de entrada real, que está reservado por el CRT para los mismos fines.

Ahora, a partir de esto, quedará claro por qué la solución de Rakis no va a funcionar: los constructores de objetos globales son llamados por el DllMain real (es decir, el punto de entrada real del dll, que es el de la página de MSDN en DllMain). habla sobre), por lo que llamar a LoadLibrary desde allí tiene exactamente el mismo efecto que invocarlo desde tu falso DllMain.

Por lo tanto, siguiendo ese consejo obtendrás los mismos efectos negativos de llamar directamente a LoadLibrary en DllMain, y también ocultarás el problema en una posición aparentemente no relacionada, lo que hará que el próximo mantenedor trabaje duro para encontrar dónde está este error. situado.

En cuanto a la carga de retraso: puede ser una idea, pero debe tener mucho cuidado de no llamar a ninguna función del dll referenciado en su DllMain: de hecho, si lo hiciera, activaría una llamada oculta a LoadLibrary, que tendría el mismo efectos negativos de llamarlo directamente.

De todos modos, en mi opinión, si necesita referirse a algunas funciones en una dll, la mejor opción es vincular estáticamente con su biblioteca de importación, por lo que el cargador la cargará automáticamente sin darle ningún problema, y ​​resolverá automáticamente cualquier dependencia extraña. cadena que pueda surgir.

Incluso en este caso, no debe invocar ninguna función de este dll en DllMain, ya que no está garantizado que ya se haya cargado; de hecho, en DllMain puedes confiar solo en que kernel32 se cargue, y quizás en dlls estés absolutamente seguro de que tu llamador ya cargó antes de que LoadLibrary que está cargando tu dll haya sido emitido (pero aún así no debes confiar en esto, porque su dll también puede ser cargado por aplicaciones que no coinciden con estas suposiciones, y solo quiere, por ejemplo, cargar un recurso de su dll sin llamar a su código ).

Como lo señala el artículo que relacioné antes,

El problema es que, en lo que respecta a tu binario, DllMain recibe un llamado en un momento verdaderamente único. Para entonces, el cargador del sistema operativo ha encontrado, mapeado y atado el archivo del disco, pero, dependiendo de las circunstancias, en cierto modo su binario puede no haber “nacido completamente”. Las cosas pueden ser complicadas.

En pocas palabras, cuando se llama a DllMain, el cargador del sistema operativo se encuentra en un estado bastante frágil. En primer lugar, ha aplicado un locking en sus estructuras para evitar daños internos mientras está dentro de esa llamada, y en segundo lugar, algunas de sus dependencias pueden no estar en un estado completamente cargado . Antes de que se cargue un binario, OS Loader observa sus dependencias estáticas. Si esos requieren dependencias adicionales, los mira también. Como resultado de este análisis, se presenta una secuencia en la que deben invocarse DllMains de esos binarios. Es bastante inteligente sobre las cosas y en la mayoría de los casos incluso puede salirse con la suya si no sigue la mayoría de las reglas descritas en MSDN, pero no siempre .

El problema es que la orden de carga es desconocida para usted , pero lo más importante es que está construida en base a la información de importación estática. Si se produce alguna carga dinámica en su DllMain durante DLL_PROCESS_ATTACH y está realizando una llamada saliente, todas las apuestas están desactivadas . No hay garantía de que se invocará DllMain de ese binario y, por lo tanto, si luego intenta GetProcAddress en una función dentro de ese binario, los resultados son completamente impredecibles ya que las variables globales pueden no haberse inicializado. Lo más probable es que obtenga un AV.

(de nuevo, énfasis añadido)

Por cierto, en la pregunta de Linux vs Windows: no soy un experto en progtwigción de sistemas Linux, pero no creo que las cosas sean tan diferentes a este respecto.

Todavía hay algunos equivalentes de DllMain (las funciones _init y _fini ), que son – ¡qué casualidad! – automáticamente tomada por el CRT, que a su vez, desde _init , llama a todos los constructores para los objetos globales y las funciones marcadas con __attribute__ constructor (que de alguna manera son el equivalente del DllMain “falso” proporcionado al progtwigdor en Win32). Un proceso similar continúa con los destructores en _fini .

Como también se invoca _init mientras la carga dll aún se está llevando a cabo ( dlopen aún no ha regresado), creo que estás sujeto a limitaciones similares en lo que puedes hacer allí. Aún así, en mi opinión sobre Linux, el problema se siente menos, porque (1) tiene que optar explícitamente por una función similar a DllMain, por lo que no está inmediatamente tentado a abusar de ella y (2), las aplicaciones Linux, por lo que yo veo, tienden a usar menos carga dinámica de dlls.

En una palabra

Ningún método “correcto” le permitirá hacer referencia a cualquier dll que no sea kernel32.dll en DllMain.

Por lo tanto, no haga nada importante desde DllMain, ni directamente (es decir, en “su” DllMain llamado por el CRT) ni indirectamente (en clase global / constructores de campos estáticos), especialmente no cargue otros dlls , nuevamente, ni directamente ( a través de LoadLibrary) ni indirectamente (con llamadas a funciones en dlls cargados por retraso, que activan una llamada LoadLibrary).

La forma correcta de tener otro dll cargado como una dependencia es ¡doh! – marcarlo como una dependencia estática. Solo haga un enlace contra su biblioteca de importación estática y haga referencia al menos a una de sus funciones: el enlazador lo agregará a la tabla de dependencias de la imagen ejecutable, y el cargador lo cargará automáticamente (inicializándolo antes o después de la llamada a su DllMain, no es necesario que lo sepa porque no debe llamarlo desde DllMain).

Si esto no es viable por alguna razón, todavía hay opciones de carga de retraso (con los límites que dije antes).

Si todavía , por algún motivo desconocido, tienes la inexplicable necesidad de llamar a LoadLibrary en DllMain, bueno, adelante, dispara en tu pie, está en tus facultades. Pero no me digas que no te advertí.


Me estaba olvidando: otra fuente fundamental de información sobre el tema es el documento Best Practices for Creating DLLs de Microsoft, que en realidad habla casi solo sobre el cargador, DllMain, el locking del cargador y sus interacciones; échale un vistazo para obtener información adicional sobre el tema.


Apéndice

No, realmente no es una respuesta a mi pregunta. Todo lo que dice es: “No es posible con enlaces dynamics, debe vincular estáticamente”, y “no debe llamar desde dllmain”.

Cuál es la respuesta a su pregunta: bajo las condiciones que impuso, no puede hacer lo que quiera. En pocas palabras, desde DllMain no puedes llamar a nada más que a las funciones kernel32 . Período.

Aunque en detalle, pero no estoy realmente interesado en por qué no funciona,

Deberías, en cambio, porque entender por qué las reglas están hechas de esa manera te hace evitar grandes errores.

El hecho es que el cargador no está resolviendo las dependencias correctamente y el proceso de carga está incorrectamente enhebrado por parte de Microsoft.

No, cariño, el cargador hace su trabajo correctamente, porque después de que LoadLibrary ha regresado, todas las dependencias se cargan y todo está listo para ser utilizado. El cargador intenta llamar al DllMain en orden de dependencia (para evitar problemas con dlls rotos que dependen de otros dlls en DllMain), pero hay casos en los que esto es simplemente imposible.

Por ejemplo, puede haber dos dlls (por ejemplo, A.dll y B.dll) que dependen el uno del otro: ahora, ¿quién debe llamar primero a DllMain? Si el cargador inicializó A.dll primero, y esto, en su DllMain, llamó a una función en B.dll, cualquier cosa podría pasar, ya que B.dll aún no se ha inicializado (aún no se ha llamado a DllMain). Lo mismo aplica si invertimos la situación.

Puede haber otros casos en los que puedan surgir problemas similares, por lo que la regla simple es: no invoque ninguna función externa en DllMain, DllMain solo sirve para inicializar el estado interno de su dll.

El problema es que no hay otra forma de hacerlo en dll_attach, y todas las buenas conversaciones sobre no hacer nada allí son superfluas, porque no hay alternativa, al menos no en mi caso.

Esta discusión sigue así: dices “Quiero resolver una ecuación como x ^ 2 + 1 = 0 en el dominio real”. Todo el mundo te dice que no es posible; dices que no es una respuesta, y culpas a las matemáticas.

Alguien te dice: hey, puedes, he aquí un truco, la solución es solo +/- sqrt (-1); todos rechazan esta respuesta (porque está mal para su pregunta, estamos yendo fuera del dominio real), y culpan a los que votan negativamente. Te explico por qué esa solución no es correcta según tu pregunta y por qué este problema no se puede resolver en el dominio real. Usted dice que no le importa por qué no se puede hacer, que solo puede hacer eso en el dominio real y nuevamente culpar a las matemáticas.

Ahora, dado que, como se explicó y reformuló un millón de veces, bajo sus condiciones su respuesta no tiene solución , ¿puede explicarnos por qué demonios “tiene” que hacer algo tan estúpido como cargar un dll en DllMain ? A menudo surgen problemas “imposibles” porque hemos elegido una ruta extraña para resolver otro problema, lo que nos lleva a un punto muerto. Si explicó el outlook general, podríamos sugerirle una mejor solución que no implique cargar dlls en DllMain.

PD: Si enlace estáticamente DLL2 (ole32.dll, Vista x64) contra DLL1 (mydll), ¿qué versión del dll requerirá el enlazador en los sistemas operativos más antiguos?

El que está presente (obviamente estoy asumiendo que estás comstackndo durante 32 bits); si una función exportada necesitada por su aplicación no está presente en el dll encontrado, su dll simplemente no está cargada (LoadLibrary falla).


Adición (2)

Positivo en la inyección, con CreateRemoteThread si quieres saber. Solo en Linux y Mac, la biblioteca dll / compartida es cargada por el cargador.

Agregar el dll como una dependencia estática (lo que se ha sugerido desde el principio) hace que el cargador lo cargue exactamente como lo hace Linux / Mac, pero el problema sigue ahí, ya que, como expliqué, en DllMain aún no puede confiar en cualquier cosa que no sea kernel32.dll (incluso si el cargador en general es lo suficientemente inteligente como para iniciar primero las dependencias).

Aún así, el problema puede ser resuelto. Cree el hilo (que en realidad llama a LoadLibrary para cargar su dll) con CreateRemoteThread; en DllMain use algún método IPC (por ejemplo memoria compartida con nombre, cuyo identificador se guardará en algún lugar para que se cierre en la función init) para pasar al progtwig inyector la dirección de la función init “real” que proporcionará su dll. DllMain luego saldrá sin hacer nada más. La aplicación del inyector, en cambio, esperará al final del subproceso remoto con WaitForSingleObject utilizando el identificador proporcionado por CreateRemoteThread. Luego, después de que el hilo remoto termine (así LoadLibrary se completará, y todas las dependencias se inicializarán), el inyector leerá de la memoria compartida nombrada creada por DllMain la dirección de la función init en el proceso remoto, y comenzará con CreateRemoteThread.

Problema: en Windows 2000, el uso de objetos nombrados de DllMain está prohibido porque

En Windows 2000, la DLL de Servicios de Terminal proporciona los objetos con nombre. Si este archivo DLL no se inicializa, las llamadas a la DLL pueden hacer que el proceso se bloquee.

Por lo tanto, esta dirección puede tener que pasar de otra manera. Una solución bastante limpia sería crear un segmento de datos compartido en el dll, cargarlo tanto en la aplicación del inyector como en el objective y poner en dicho segmento de datos la dirección requerida. El dll obviamente tendría que cargarse primero en el inyector y luego en el objective, porque de lo contrario se sobrescribirá la dirección “correcta”.

Otro método realmente interesante que se puede hacer es escribir en la otra memoria de proceso una pequeña función (directamente en el ensamblaje) que llama LoadLibrary y devuelve la dirección de nuestra función init; ya que lo escribimos allí, también podemos llamarlo CreateRemoteThread porque sabemos dónde está.

En mi opinión, este es el mejor enfoque, y también es el más simple, ya que el código ya está allí, escrito en este bonito artículo . Echa un vistazo, es bastante interesante y probablemente sea el truco para tu problema.

La forma más robusta es vincular la primera DLL con la lib de importación de la segunda. De esta forma, la carga real de la segunda DLL será realizada por el propio Windows. Suena muy trivial, pero no todo el mundo sabe que las DLL pueden vincularse con otras DLL. Windows puede incluso tratar con dependencias cíclicas. Si A.DLL carga B.DLL que necesita A.DLL, las importaciones en B.DLL se resuelven sin cargar A.DLL de nuevo.

Te sugiero que uses mecanismo de carga de retraso. La DLL se cargará en el momento preciso en que llame a la función importada. Además, puede modificar la función de carga y el manejo de errores. Consulte la compatibilidad de Linker para DLL cargadas de retraso para obtener más información.

Una posible respuesta es mediante el uso de LoadLibrary y GetProcAddress para acceder a los punteros a las funciones encontradas / ubicadas dentro del dll cargado, pero sus intenciones / necesidades no son lo suficientemente claras como para determinar si esta es una respuesta adecuada.