Caricamento di una DLL da una DLL?

Qual è il modo migliore per caricare una DLL da una DLL?

Il mio problema è che non riesco a caricare una DLL su process_attach, e non riesco a caricare la DLL dal programma principale, perché non controllo la sorgente del programma principale. E quindi non posso chiamare anche una funzione non-dllmain.

Dopo tutto il dibattito che è andato avanti nei commenti, penso che sia meglio riassumere le mie posizioni in una risposta “reale”.

Prima di tutto, non è ancora chiaro il motivo per cui è necessario caricare una DLL in DllMain con LoadLibrary. Questa è sicuramente una ctriggers idea, poiché il tuo DllMain è in esecuzione all’interno di un’altra chiamata a LoadLibrary, che contiene il blocco del caricatore, come spiegato dalla documentazione di DllMain :

Durante l’avvio del processo iniziale o dopo una chiamata a LoadLibrary, il sistema esegue la scansione dell’elenco di DLL caricate per il processo. Per ogni DLL che non è stata già chiamata con il valore DLL_PROCESS_ATTACH, il sistema chiama la funzione del punto di ingresso della DLL. Questa chiamata viene effettuata nel contesto del thread che ha causato la modifica dello spazio indirizzo del processo, ad esempio il thread principale del processo o il thread che ha chiamato LoadLibrary. L’accesso al punto di ingresso è serializzato dal sistema a livello di processo. I thread in DllMain mantengono il blocco del programma di caricamento in modo che nessuna DLL aggiuntiva possa essere caricata o inizializzata in modo dinamico.

La funzione del punto di ingresso dovrebbe eseguire solo semplici attività di inizializzazione o di terminazione . Non deve chiamare la funzione LoadLibrary o LoadLibraryEx (o una funzione che chiama queste funzioni) , poiché ciò potrebbe creare loop di dipendenza nell’ordine di caricamento della DLL. Ciò può comportare l’utilizzo di una DLL prima che il sistema abbia eseguito il codice di inizializzazione. Allo stesso modo, la funzione del punto di ingresso non deve chiamare la funzione FreeLibrary (o una funzione che chiama FreeLibrary) durante la chiusura del processo, poiché ciò può provocare l’utilizzo di una DLL dopo che il sistema ha eseguito il codice di terminazione.

(enfasi aggiunta)

Quindi, questo sul perché è proibito; per una spiegazione chiara e più approfondita, vedi questo e questo , per qualche altro esempio su cosa può accadere se non rispetti queste regole in DllMain vedi anche alcuni post nel blog di Raymond Chen .

Ora, su Rakis rispondi.

Come ho già ripetuto più volte, ciò che pensi sia DllMain, non è il vero DllMain della DLL; invece, è solo una funzione che viene chiamata dal vero e proprio entrypoint della dll. Questo, a sua volta, viene automaticamente preso dal CRT per eseguire i suoi ulteriori compiti di inizializzazione / pulizia, tra cui vi è la costruzione di oggetti globali e dei campi statici delle classi (in realtà tutti questi dal punto di vista del compilatore sono quasi gli stessi cosa). Dopo (o prima, per la pulizia) completa queste attività, chiama il tuo DllMain.

Va in qualche modo come questo (ovviamente non ho scritto tutta la logica di controllo degli errori, è solo per mostrare come funziona):

/* 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; } 

Non c’è nulla di speciale in questo: succede anche con i normali eseguibili, con il tuo principale chiamato dal vero e proprio entrypoint, che è riservato dal CRT per gli stessi identici scopi.

Ora, da questo sarà chiaro il motivo per cui la soluzione di Rakis non funzionerà: i costruttori per gli oggetti globali sono chiamati dal vero DllMain (ovvero l’effettivo punto di accesso della dll, che è quello della pagina MSDN su DllMain parla di), quindi chiamare LoadLibrary da lì ha esattamente lo stesso effetto di chiamarlo dal tuo falso-DllMain.

Quindi, seguendo questo consiglio otterrai gli stessi effetti negativi di chiamare direttamente LoadLibrary nel DllMain, e nasconderai il problema anche in una posizione apparentemente non correlata, il che farà sì che il prossimo manutentore lavori duramente per trovare dove si trova questo bug trova.

Per quanto riguarda il delayload: può essere un’idea, ma devi stare molto attento a non chiamare alcuna funzione della dll di riferimento nel tuo DllMain: infatti, se lo facessi, avresti triggersto una chiamata nascosta a LoadLibrary, che avrebbe lo stesso effetti negativi di chiamarlo direttamente.

Comunque, secondo me, se hai bisogno di riferirti ad alcune funzioni in una DLL, l’opzione migliore è di colbind staticamente alla sua libreria di importazione, così il caricatore lo caricherà automaticamente senza darti alcun problema e risolverà automaticamente ogni strana dipendenza catena che potrebbe sorgere.

Anche in questo caso non devi chiamare alcuna funzione di questa dll in DllMain, poiché non è garantito che sia già stata caricata; in realtà, in DllMain puoi fare affidamento solo sul kernel32 che viene caricato, e forse su dll sei assolutamente sicuro che il tuo chiamante sia già stato caricato prima che la LoadLibrary che sta caricando la tua DLL sia stata emessa (ma ancora non dovresti fare affidamento su questo perché la tua DLL può anche essere caricata da applicazioni che non corrispondono a questi presupposti e vogliono solo, ad esempio, caricare una risorsa della tua DLL senza chiamare il tuo codice ).

Come sottolineato dall’articolo che ho collegato prima,

Il problema è che, per quanto riguarda il tuo binario, DllMain viene chiamato in un momento davvero unico. A quel punto il caricatore del sistema operativo ha trovato, mappato e associato il file dal disco, ma – a seconda delle circostanze – in un certo senso il tuo file binario potrebbe non essere stato “completamente nato”. Le cose possono essere complicate.

In poche parole, quando si chiama DllMain, il caricatore del sistema operativo si trova in uno stato piuttosto fragile. Prima di tutto, ha applicato un blocco sulle sue strutture per prevenire la corruzione interna durante la chiamata, e in secondo luogo, alcune delle tue dipendenze potrebbero non essere in uno stato completamente caricato . Prima che un binario venga caricato, OS Loader controlla le sue dipendenze statiche. Se quelli richiedono dipendenze aggiuntive, li guarda pure. Come risultato di questa analisi, viene visualizzata una sequenza in cui è necessario chiamare DllMains di quei file binari. È abbastanza intelligente per le cose e nella maggior parte dei casi puoi persino cavarcanvas senza seguire la maggior parte delle regole descritte in MSDN – ma non sempre .

Il fatto è che l’ordine di caricamento non è noto a te , ma soprattutto, è basato sulle informazioni di importazione statica. Se si verifica un caricamento dinamico nel DllMain durante DLL_PROCESS_ATTACH e si sta effettuando una chiamata in uscita, tutte le scommesse sono distriggerste . Non è garantito che DllMain di tale binario venga chiamato e quindi se si tenta di ottenere GetProcAddress in una funzione all’interno di tale binario, i risultati sono completamente imprevedibili poiché le variabili globali potrebbero non essere state inizializzate. Molto probabilmente otterrai un AV.

(di nuovo, enfasi aggiunta)

A proposito, sulla domanda Linux vs Windows: non sono un esperto di programmazione di sistemi Linux, ma non penso che le cose siano così diverse lì sotto questo aspetto.

Ci sono ancora alcuni equivalenti di DllMain (le funzioni _init e _fini ), che sono – che coincidenza! – preso automaticamente dal CRT, che a sua volta, da _init , chiama tutti i costruttori per gli oggetti globali e le funzioni contrassegnate con __attribute__ constructor (che sono in qualche modo l’equivalente del DllMain “falso” fornito al programmatore in Win32). Un processo simile continua con i distruttori in _fini .

Poiché _init viene chiamato anche mentre il caricamento della DLL è ancora in corso ( dlopen non è ancora tornato), penso che tu sia sobject a limitazioni simili a ciò che puoi fare lì. Comunque, secondo me su Linux il problema si fa sentire meno, perché (1) devi esplicitamente opt-in per una funzione simile a DllMain, quindi non sei immediatamente tentato di abusarne e (2), le applicazioni Linux, per quanto ho visto, tendono ad usare meno caricamento dinamico di DLL.

In poche parole

Nessun metodo “corretto” consentirà di fare riferimento a qualsiasi DLL diversa da kernel32.dll in DllMain.

Quindi, non fare nulla di importante da DllMain, né direttamente (cioè nel “tuo” DllMain chiamato dal CRT) né indirettamente (in costruttori di campi / classi statici globali), specialmente non caricare altre DLL , di nuovo, né direttamente ( tramite LoadLibrary) né indirettamente (con chiamate alle funzioni nelle DLL con ritardo di caricamento, che triggersno una chiamata LoadLibrary).

Il modo giusto per avere un’altra DLL caricata come dipendenza è fare – do! – contrassegnalo come dipendenza statica. Collegati alla sua libreria di importazione statica e fai riferimento ad almeno una delle sue funzioni: il linker la aggiungerà alla tabella delle dipendenze dell’immagine eseguibile e il caricatore lo caricherà automaticamente (inizializzandolo prima o dopo la chiamata al tuo DllMain, tu non ho bisogno di saperlo perché non devi chiamarlo da DllMain).

Se questo non è fattibile per qualche motivo, ci sono ancora le opzioni di delayload (con i limiti che ho detto prima).

Se ancora , per qualche motivo sconosciuto, hai l’inspiegabile necessità di chiamare LoadLibrary in DllMain, beh, vai avanti, spara ai tuoi piedi, è nelle tue facoltà. Ma non dirmi che non ti avevo avvertito.


Stavo dimenticando: un’altra fonte fondamentale di informazioni sull’argomento è il documento Best Practices for Creating DLL di Microsoft, che in realtà parla quasi solo del caricatore, DllMain, del lock loader e delle loro interazioni; dare un’occhiata a questo per ulteriori informazioni sull’argomento.


appendice

No, non è davvero una risposta alla mia domanda. Tutto ciò che dice è: “Non è ansible con il collegamento dinamico, è necessario colbind staticamente”, e “non si può chiamare da dllmain”.

Qual è la risposta alla tua domanda: alle condizioni che hai imposto, non puoi fare quello che vuoi. In breve, da DllMain non è ansible chiamare nient’altro che funzioni kernel32 . Periodo.

Anche se nei dettagli, ma non sono davvero interessato al motivo per cui non funziona,

Dovresti, invece, perché capire perché le regole sono fatte in quel modo ti fa evitare grandi errori.

Infatti, il caricatore non risolve correttamente le dipendenze e il processo di caricamento non è correttamente filettato da parte di Microsoft.

No, mia cara, il caricatore fa il suo lavoro correttamente, perché dopo che LoadLibrary è tornato, tutte le dipendenze sono caricate e tutto è pronto per essere utilizzato. Il programma di caricamento tenta di chiamare il DllMain in ordine di dipendenza (per evitare problemi con DLL non funzionanti che si basano su altre DLL in DllMain), ma ci sono casi in cui ciò è semplicemente imansible.

Ad esempio, potrebbero esserci due dll (ad esempio, A.dll e B.dll) che dipendono l’una dall’altra: ora, di chi DllMain deve chiamare per primo? Se il caricatore ha inizializzato A.dll per primo e questo, nel suo DllMain, ha chiamato una funzione in B.dll, potrebbe succedere di tutto, poiché B.dll non è ancora stato inizializzato (il suo DllMain non è stato ancora chiamato). Lo stesso vale se invertiamo la situazione.

Ci possono essere altri casi in cui possono sorgere problemi simili, quindi la semplice regola è: non chiamare alcuna funzione esterna in DllMain, DllMain serve solo per inizializzare lo stato interno della tua DLL.

Il problema è che non c’è altro modo di farlo su dll_attach, e tutto il bel discorso sul non fare nulla è superfluo, perché non c’è alternativa, almeno non nel mio caso.

Questa discussione sta procedendo in questo modo: tu dici “Voglio risolvere un’equazione come x ^ 2 + 1 = 0 nel dominio reale”. Tutti ti dicono che non è ansible; tu dici che non è una risposta e incolpare la matematica.

Qualcuno ti dice: hey, puoi, ecco un trucco, la soluzione è solo +/- sqrt (-1); tutti mollerano questa risposta (perché è sbagliata per la tua domanda, stiamo andando fuori dal vero dominio) e tu dai la colpa a chi downvotes. Ti spiego perché questa soluzione non è corretta in base alla tua domanda e perché questo problema non può essere risolto nel dominio reale. Dici che non ti importa del perché non può essere fatto, che puoi farlo solo nel dominio reale e di nuovo incolpare la matematica.

Ora, dal momento che, come spiegato e ripetuto un milione di volte, alle tue condizioni la tua risposta non ha soluzione , puoi spiegarci perché mai “devi” fare una cosa così idiota come caricare una DLL in DllMain ? Spesso sorgono problemi “impossibili” perché abbiamo scelto una strada strana per risolvere un altro problema, il che ci porta al punto morto. Se hai spiegato l’immagine più grande, potremmo suggerire una soluzione migliore che non implichi il caricamento di dll in DllMain.

PS: Se collego staticamente DLL2 (ole32.dll, Vista x64) a DLL1 (mydll), quale versione della DLL richiede il linker su sistemi operativi precedenti?

Quello che è presente (ovviamente presumo che tu stia compilando per 32 bit); se una funzione esportata richiesta dall’applicazione non è presente nella dll trovata, la tua DLL non viene semplicemente caricata (LoadLibrary ha esito negativo).


Addendum (2)

Positivo all’iniezione, con CreateRemoteThread se vuoi sapere. Solo su Linux e Mac la libreria dll / condivisa viene caricata dal loader.

Aggiungendo la DLL come dipendenza statica (ciò che è stato suggerito sin dall’inizio) lo fa caricare dal loader esattamente come fa Linux / Mac, ma il problema è ancora lì, poiché, come ho spiegato, in DllMain non si può ancora contare su qualsiasi cosa diversa da kernel32.dll (anche se il loader in generale è abbastanza intelligente da inizializzare prima le dipendenze).

Tuttavia, il problema può essere risolto. Crea il thread (che in realtà chiama LoadLibrary per caricare la tua DLL) con CreateRemoteThread; in DllMain utilizzare un metodo IPC (ad esempio, la memoria condivisa denominata, il cui handle verrà salvato da qualche parte per essere chiuso nella funzione init) per passare al programma dell’iniettore l’indirizzo della funzione di inizializzazione “reale” fornita dalla DLL. DllMain quindi uscirà senza fare altro. L’applicazione injector, invece, attenderà la fine del thread remoto con WaitForSingleObject utilizzando l’handle fornito da CreateRemoteThread. Quindi, una volta terminato il thread remoto (quindi LoadLibrary verrà completato e tutte le dipendenze verranno inizializzate), l’iniettore leggerà dalla memoria condivisa denominata creata da DllMain l’indirizzo della funzione init nel processo remoto e inizierà con CreateRemoteThread.

Problema: su Windows 2000 l’utilizzo di oggetti denominati da DllMain è vietato perché

In Windows 2000, gli oggetti denominati vengono forniti dalla DLL di Servizi terminal. Se questa DLL non è inizializzata, le chiamate alla DLL possono causare il blocco del processo.

Quindi, questo indirizzo potrebbe dover essere passato in un altro modo. Una soluzione abbastanza pulita sarebbe quella di creare un segmento di dati condiviso nella DLL, caricarlo sia nell’applicazione injector che in quella di destinazione e far sì che inserisca in tale segmento di dati l’indirizzo richiesto. La DLL dovrebbe ovviamente essere caricata prima nell’iniettore e poi nel bersaglio, altrimenti l’indirizzo “corretto” verrebbe sovrascritto.

Un altro metodo davvero interessante che si può fare è scrivere nell’altra memoria di processo una piccola funzione (direttamente in assembly) che chiama LoadLibrary e restituisce l’indirizzo della nostra funzione init; dato che l’abbiamo scritto lì, possiamo anche chiamarlo con CreateRemoteThread perché sappiamo dove si trova.

Secondo me, questo è l’approccio migliore, ed è anche il più semplice, dal momento che il codice è già lì, scritto in questo bell’articolo . Date un’occhiata, è piuttosto interessante e probabilmente farà il trucco per il vostro problema.

Il modo più efficace consiste nel colbind la prima DLL alla lib di importazione del secondo. In questo modo, il caricamento effettivo della seconda DLL verrà eseguito da Windows stesso. Sembra molto banale, ma non tutti sanno che le DLL possono essere collegate ad altre DLL. Windows può anche gestire le dipendenze cicliche. Se A.DLL carica B.DLL che richiede A.DLL, le importazioni in B.DLL vengono risolte senza caricare nuovamente A.DLL.

Ti suggerisco di utilizzare il meccanismo di caricamento ritardato. La DLL verrà caricata nel momento in cui si chiama la funzione importata. Inoltre è ansible modificare la funzione di carico e la gestione degli errori. Vedi il supporto del linker per le DLL con ritardo di caricamento per maggiori informazioni.

Una ansible risposta è tramite l’uso di LoadLibrary e GetProcAddress per accedere ai puntatori alle funzioni trovate / posizionate all’interno della DLL caricata, ma le vostre intenzioni / esigenze non sono sufficientemente chiare per determinare se questa è una risposta adatta.