Home | Chi sono | Contattami
 

Progr. lineare

Delphi
 
Componenti
  Database
 
Miei articoli

Windows

Miei articoli 

 

PE Imports


In questo articolo parleremo un pò dell'utilizzo di funzioni implementate in altri moduli. Prendiamo il caso (non raro del resto) che voglio usare l'api win32 MessageBoxA (che mi consente di visualizzare un messaggio a video tramite una semplice finestrella) che è implementata nella dll kernel32.dll: basta che includo la unit Windows ed il gioco è fatto direte voi. Si certo ma cosa c'è nella unit Windows che fa capire al mio programmino che, nel momento in cui scrivo MessageBoxA voglio chiamare la funzione MessageBoxA implementata in kernel32.dll? La risposta sta nelle seguenti istruzioni

... interface ... const ... user32 = 'user32.dll'; ... function MessageBoxA( hWnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT ): Integer; stdcall; ... implementation ... function MessageBoxA; external user32 name 'MessageBoxA'; ...

che possono essere tradotte nella più immediata e corrispondente dichiarazione

function MessageBoxA( hWnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT ): Integer; stdcall; external 'user32.dll' name 'MessageBoxA';

La dichiarazione sta ad indicare che ho importato nel mio programma la funzione MessageBoxA esportata dalla dll user32.dll. La sintassi generica di importazione di una funzione da un modulo esterno è la seguente

<dichiarazione funzione> external <nome_modulo> name <nome_funzione>

<nome_modulo> è appunto il nome della dll che esporta la funzione che vogliamo importare e <nome_funzione> è il nome della funzione in questione; in alternativa si può far riferimento alla funzione in questione tramite il suo Ordinal ed in tal caso useremo la seguente sintassi:

<dichiarazione funzione> external <nome_modulo> index <Ordinal_funzione>

Comunque la cosa migliore è sempre far riferimento al nome della funzione (così come è bene esportare le funzioni da una dll di nostra creazione sempre per nome): naturalmente l'unica eccezione si ha per causa di forze maggiori ossia quando la funzione non è stata esportata per nome (in questo caso o ci attacchiamo all'Ordinal o ci attacchiamo al c**...).

Si noti inoltre che il nome che diamo alla funzione nella dichiarazione non deve necessariamente corrispondere al nome di esportazione della funzione (behh ... è chiaro che deve per forza essere svincolato, altrimenti che nome daremmo alle funzioni importate che nel loro modulo di implementazione sono state esportate senza nome e di cui quindi abbiamo solo l'Ordinal). Un esempio è la MessageBox, dichiarata sempre nella unit Windows e che fa sempre riferimento alla MessageBoxA esportata da kernel32.dll

function MessageBox( hWnd: HWND; lpText, lpCaption: PAnsiChar; uType: UINT ): Integer; stdcall; external 'user32.dll' name 'MessageBoxA';

Ma cosa significa a basso livello importare una funzione? Cosa succede nel mio modulo (che può essere un .exe ma anche una dll) quando decido di importare una funzione? Si può partire dal seguente grafico

Nell'analizzare questo grafico è opportuno tenere conto delle nozioni esposte negli articoli articolo82.htm e articolo83.htm relativamente alla Export Directory. E adesso andiamo al sodo. Nella DataDirectory in posizione 8 abbiamo 4 byte che contengono l'RVA di un array di oggetti di tipo IMAGE_IMPORT_DESCRIPTOR. Ogni elemento dell'array è appunto un oggetto di tipo IMAGE_IMPORT_DESCRIPTOR che contiene informazioni relative ad un modulo da cui andiamo ad importare una o più funzioni. Ci saranno quindi tanti elementi quanti sono i moduli da cui importiamo una o più funzioni. Non essendoci alcun campo altrove che definisce il numero di tali valori, viene inserito un elemento nullo in ultima posizione. Analizziamo il tipo IMAGE_IMPORT_DESCRIPTOR

  • ORIGINAL_FIRST_THUNK: RVA dell' array comunemente chiamato Import Lookup Table (ILT).
  • TIMEDATESTAMP: correlato al concetto di Binding di un modulo (che vedremo in seguito) ma trascurabile in quanto legato alla vecchia metodologia di gestione delle immagine Bindate da parte del Loader di Windows
  • FORWARDERCHAIN: discorso uguale al campo TIMEDATESTAMP
  • NAME: RVA della stringa null terminated (sequenza di caratteri che termina con '\0') che definisce il nome del modulo importato. 
  • FIRST_THUNK: RVA dell'array comunemente chiamato Import Address Table (IAT).

Se ad esempio abbiamo un .exe che importa funzioni dai moduli kernel32.dll, user32.dll e advapi.dll, avremo 4 elementi nell'array dei Descriptor (consideriamo anche l'elemento nullo di terminazione) ed il campo Name punterà rispettivamente ai valori 'kernel32.dll\0', 'user32.dll\0' e 'advapi.dll\0'.

Analizziamo ora gli array IMPORT_ADDRESS_TABLE e IMPORT_LOOKUP_TABLE. Prima di tutto occorre dire che l'array fondamentale è l'IMPORT_ADDRESS_TABLE; in pratica l'IMPORT_LOOKUP_TABLE può non esistere e quindi il campo IMAGE_IMPORT_DESCRIPTOR.ORIGINAL_FIRST_THUNK contiene $00000000. Gaurdacaso questo è proprio il comportamento del linker Borland: i binary creati con Delphi o C++ Builder non hanno IMPORT_LOOKUP_TABLE. Ma allora a cosa serve definire questo array? Lo vedremo nel corso della discussione. Ma prima di tutto cosa contiene l'IMPORT_ADDRESS_TABLE? La risposta è semplice: tanti elementi quante sono le funzioni importate dal modulo a cui fa riferimento. Ogni elemento è un oggetto di tipo IMAGE_THUNK_DATA: questo tipo ha dimensione 4 byte nei moduli a 32 bit (comunemente detti PE) oppure 8 byte nei moduli a 64 bit (PE+) (chiaramente i PE+ possono essere usati solo nei sistemi Windows a 64 bit i quali a loro volta possono girare solo su architetture con processori a 64 bit); per convenienza faremo riferimento al contesto a 32 bit. Il valore assunto varia a seconda che si parli del modulo presente su disco o della sua mappatura da parte del Loader di Windows. Vediamo di andare un pò più nei dettagli.

Il linker (il programma adibito alla creazione del binary .exe o .dll) crea la tabella dei Descriptor: tale tabella conterrà tanti elementi quanti sono i moduli da cui si importano funzioni (più l'elemento nullo finale naturalmente). Per ogni Descriptor verrà raccolto nell'array IMPORT_ADDRESS_TABLE l'elenco delle funzioni importate dal modulo corrispondente. A seconda del tipo di linker, le medesime informazioni possono essere raccolte anche nell'array IMPORT_LOOKUP_TABLE: in pratica se il linker annovera anche la definizione dell'IMPORT_LOOKUP_TABLE (non è il caso di Delphi e C++ Builder), questi 2 array alla fine avranno lo stesso contenuto. Come si può vedere dal grafico, anche nel caso dei 2 array in questione, viene inserito un elemento nullo in fondo. Ma cosa contengono? Contengono il riferimento alla funzione da importare che può essere l'Ordinal oppure il nome della funzione medesima. Come si può vedere dal grafico, se il primo bit da sinistra (Most Significant Bit o più brevemente MSB) è settato ad 1 allora abbiamo l'Ordinal; in caso contrario abbiamo l'RVA ad un record di tipo IMAGE_IMPORT_BY_NAME: i primi 2 byte (campo Hint) rappresentano l'indice della funzione nell'array degli indirizzi delle funzioni esportate (vedi i 2 articoli sulla Export Directory), mentre i restanti byte contengono una stringa null terminated che rappresenta appunto il nome della funzione. Il campo Hint non è obbligatorio e può essere omesso da alcuni linker, anche se comunque rende più rapida la restituzione dell'RVA della funzione nel modulo di implementazione. Per verificare il settaggio del MSB basta fare la seguente semplice operazione

valore and $80000000

se il risultato non è 0 allora il MSB è settato ad 1; $80000000 è appunto la rappresentazione nel sistema esadecimale di una sequenza di bit il cui MBS è settato ad 1 e tutti gli altri bit sono 0.  

Ok. Ora cosa succede quando il modulo viene caricato dal Loader di Windows? Behh ... dopo aver mappato il modulo in questione, il Loader prosegue scansionando il suo array dei Descriptor e carica ognuno dei moduli corrispondenti (e per ognuno di essi fa la stessa cosa e così via ricorsivamente). Una volta mappati tutti i moduli in relazione alle varie tabelle dei Descriptor, gli elementi dell'IMPORT_ADDRESS_TABLE vengono scansionati e ognuno di essi viene sovrascritto col VirtualAddress corrispondente: tale Virtual Address viene calcolato navigando nella ExportDirectory del modulo che esporta la funzione (che poi è il meccanismo di funzionamento della GetProcAddress, e per maggiori informazioni si possono guardare i 2 articolo sulla Export Directory). L'IMPORT_LOOKUP_TABLE, se presente, rimane invece invariato. Tutte le chiamate a funzioni importate vengono espresse nella seguente forma:

call dword ptr[$xxxxxxxx]

$xxxxxxxx rappresenta l'RVA dell'elemento della IMPORT_ADDRESS_TABLE corrispondente alla funzione in questione: proprio in base a quanto appena detto, tale elemento conterrà il Virtual Address della funzione in questione. 

Ok, abbiamo spiegato parecchie cose ma resta ancora un interrogativo: a cosa serve avere i 2 array?? Non basta L'IMPORT_ADDRESS_TABLE? L'IMPORT_LOOKUP_TABLE che funzionalità ha? La risposta è Binding. Con il Binding, vengono eseguite direttamente sul file (.exe o .dll) quelle sostituzioni sui valori delle varie IMPORT_ADDRESS_TABLE che vengono di norma effettuate dal Loader di Windows. Esiste l'eseguibile Bind.exe incluso nel Platform SDK ed anche delle api win32 dedicate: BindImage e BindImageEx implementate in Imagehlp.dll. Si, certo, ho un binary con già i Virtual Address delle funzioni importate, ma ancora non capisco a cosa serva l'array IMPORT_LOOKUP_TABLE; il fatto è che quei Virtual Address che mi trovo direttamente nelle IMPORT_ADDRESS_TABLE di un binary Bindato vanno verificati: cosa succede infatti se anche solo una delle dll importate ha cambiato versione?? Il linea di massima sono cambiati gli RVA delle funzioni esportate e quindi anche i valori dei Virtual Address andranno ricalcolati ed è proprio qui che entra in gioco l'IMPORT_LOOKUP_TABLE che consente al Loader di Windows di eseguire con successo la rigenerazione di tutti i Virtual Address delle funzioni importate. Uhmm.... ma Delphi e C++ Builder non generano  IMPORT_LOOKUP_TABLE: questi eseguibili non sono Bindabili, a meno di non usare una utility ad hoc per la generazione delle IMPORT_LOOKUP_TABLE. Anche se non è strettamente necessario conoscere il comportamento del Loader di Windows di fronte al caricamento di un binary Bindato, è in ogni caso interessante esaminare le strutture che intervengono in questo contesto: anche in questo caso ci viene in aiuto un grafichino

     

 

Nella DataDiretory, l'entry alla posizione 88 contiene l'RVA ad un array di dati; questa entry della DataDirectory è diversa da 0 solo nel caso in cui il binary in questione è stato Bindato. Di conseguenza la presenza di un valore diverso da 0 indica inequivocabilmente il fatto che il .exe (o la dll) è stato Bindato. Ma cosa contiene quest'array? Essenzialmente ciò che serve al Loader per capire, di fronte ad un modulo da cui vengono importate delle funzioni, se è il caso di ricalcolare i valori della IMPORT_ADDRESS_TABLE corrispondente: infatti se la versione attuale del modulo è diversa dalla versione utilizzata per Bindare il nostro programma, allora quei Virtual Address non sono più validi e vanno ricalcolati partendo dalla IMPORT_LOOKUP_TABLE. Ogni elemento dell'array puntato da "Bound Import RVA" è costituito da un oggetto di tipo BOUND_IMPORT_DESCRIPTOR seguito eventualmente (e sottolineo eventualmente in quanto non è obbligatorio) da uno o più oggetti di tipo BOUND_FORWARDER_REF. Ogni oggetto di tipo BOUND_IMPORT_DESCRIPTOR fa riferimento ad un modulo dal quale il nostro programma ha importato delle funzioni. La composizione è semplicissima

  • TimeDateStamp: data/ora di ultima modifica del modulo all'atto del Binding del nostro programma; se all'atto dell'esecuzione, il Loader di Windows rileva che la versione attualmente presente sul sistema è differente (data/ora di ultima modifica differente) allora procede a ricalcolare i Virtual Address nella IMPORT_ADDRESS_TABLE corrispondente (usando come riferimento la IMPORT_LOOKUP_TABLE).
  • OffsetModuleName: indica l'offset, calcolato rispetto rispetto all'inizio dell'array puntato da "Bound Import RVA", a cui si trova la stringa null terminated che rappresenta il nome del modulo in questione; è bene sottolineare nuovamente che non è un RVA bensì la distanza dall'inizio dell'array puntato da "Bound Import RVA" 
  • NumberOfForwarderRefs: numero di oggetti di tipo BOUND_FORWARDER_REF che seguono l'oggetto in questione. Può essere naturalmente pari a 0.

Bene ora vediamo a cosa servono gli eventuali oggetti di tipo BOUND_FORWARDER_REF che seguono l'oggetto di tipo BOUND_IMPORT_DESCRIPTOR. In pratica, alcune delle funzioni che vado ad importare da una dll possono essere il forward a funzioni esportate da altre dll ed è su queste dll che va esteso il controllo sulla versione. Ad esempio il nostro programma importa funzioni da testdll.dll. Essendo stato Bindato, deve contenere una Bound Import Directory e nell'array puntato da Bound Import RVA ci deve essere un oggetto di tipo BOUND_IMPORT_DESCRIPTOR che fa riferimento a testdll.dll memorizzandone la data/ora di ultima modifica all'atto del Binding. Ora supponiamo che tra tutte le funzioni che vado ad importare da test.dll vi siano le seguenti 5 con le seguenti caratteristiche:

funz_abc:  prfsa.f55  (forward alla funzione f55 esportata da prfsa.dll)
cdrz: a1b.RVSTstFuncIO  (forward alla funzione RVSTstFuncIO  esportata da a1b.dll)
Funz99: mod98.MemTest2  (forward alla funzione MemTest2 esportata da mod98.dll)
fEx4: a1b.WWrty  (forward alla funzione WWrty  esportata da a1b.dll)
fCCTest: prfsa.H77ccra (forward alla funzione H77ccra  esportata da prfsa.dll)
 

l'oggetto di tipo BOUND_IMPORT_DESCRIPTOR corrispondente a testdll.dll dovrà essere seguito da 3 oggetti di tipo BOUND_FORWARDER_REF che fanno riferimento rispettivamente a prfsa.dll, a1b.dll e mod98.dll

La composizione del tipo BOUND_FORWARDER_REF è esattamente identica al tipo BOUND_IMPORT_DESCRIPTOR fatta eccezione per l'ultimo campo Reserved che di fatto non viene usato ma serve più che altro per rendere i 2 tipi strutturalmente identici.

Behh per il momento può bastare.

 

 

 

   

 
 
Your Ad Here