Aggiungere una DLL a un PE
IL PREAMBOLO
Autore: NaGA
Tools: Hiew, il vostro cervello (sono quasi obbligatori)
Optionals: Un compilatore C (se volete automatizzare la procedura)
L'INTRO
In principio era il caos, poi arrivarono i cracker, e le cose peggiorarono...
IL PROBLEMA
Se fate cracking o reversing (perche' i programmi, cosi' come sono, proprio non vi piacciono) o se siete dei virus coders (perche' siete bastardi dentro) vi sarete trovati nella situazione di dover chiamare delle API che non erano state previste al momento dell'assemblaggio del codice che state modificando.
A questo punto i casi sono due:
1)Avete culo e la funzione che vi serve fa cmq parte di un modulo gia' linkato a run time (es. kernel32).
2)Siete un po' sfortunati e la funzione che vi serve si trova in una DLL che il programma da modificare non usa affatto.
Se ricadete nella prima casistica e' tutto molto semplice: basta dare uno sguardo al segmento di import o usare l'API GetProcAddress (se non sbaglio viene mappata sempre, con kernel32) per ottenere l'indirizzo che vi serve.
N.B. Se non avete capito niente di quello che c'e' scritto, fatevi un giro fra gli altri tutorial sui PE, per chiarire ogni vostro dubbio.
Se invece ricadete nella seconda casistica (quella degli sfigati) le cose si complicano un po'. L'header del segmento di Import non si puo', infatti, modificare.
O almeno e' quello che ci vuole far credere il caro zio Bill...
LA SOLUZIONE
La soluzione e' quanto mai banale, e il fatto di non aver trovato nessun tutorial in proposito, probabilmente e' dovuto alla mia incapacita' nell'usare i motori di ricerca.
Il suddetto header e' indirizzato da una dword che si trova all'offset 0x80 dall'inizio dell'header PE (che dovrebbe corrispondere allo 0x100 dall'inizio del file). Quello che dovremo fare e' spostare l'header da un altra parte, aggiungere una entry relativa alla DLL che ci serve, e modificare la dword a 0x100 per farla puntare al nostro nuovo header. A questo punto potremo inserire delle nuove entry per le funzioni che ci servono (magari proprio al posto del vecchio header) o usare GetProcAddress per avere tutti gli indirizzi utili.
E' probabile che a questo punto sarete molto confusi, quindi rivediamo tutto passo passo.
-In Teoria.
L'header del segmento di import e' formato da una serie di entries e termina con una entry vuota.
Ogni Entry ha la seguente struttura:
________________________________
| |
| DD - Name LookUpTable (+0) |
| DD - UNUSED (+4) |
| DD - UNUSED (+8) |
| DD - Import Name (+12) |
| DD - Address LookUpTable (+16) |
|________________________________|
La dword a +12 punta ad una stringa (NULL terminated) che rappresenta il nome della DLL da importare.
La dword a +0 punta alla tabella dei nomi.
La dword a +16 punta alla tabella degli indirizzi.
La tabella dei nomi altro non e' che una serie di indirizzi a stringhe che rappresentano le funzioni della DLL che ci interessa usare. Ogni stringa comincia con una word (usata per distinguere fra selezione per nome o per numero, ma questo e' un'altra storia) e termina con un NULL. La tabella finisce con un indirizzo nullo.
La tabella degli indirizzi verra' riempita al momento dell'esecuzione con gli offset delle corrispondenti funzioni richiamate.
Es: Prendiamo HyperTerminal (dovreste avercelo tutti). Apritelo con Hiew. Premete F8 (per avere l'header) e poi F7 (per avere le informazioni di import/export). Selezionate Import. Dovreste vedere quanto segue...
HYPERTRM.EXE .R PE.00025000 -------- 6144 ¦ Hiew 6.04 (c)SEN
.00025000: 7C 50 00 00-80 4F 50 2A-FF FF FF FF-66 51 00 00 |P ÇOP* fQ
.00025010: C0 50 00 00-64 50 00 00-9E 4F 50 2A-FF FF FF FF +P dP ×OP*
.00025020: A0 51 00 00-A8 50 00 00-70 50 00 00-A0 1C 0C 32 áQ ¿P pP á..2
.00025030: FF FF FF FF-D4 51 00 00-B4 50 00 00-00 00 00 00 ÈQ ¦P
.00025040: 00 00 00 00-00 00 00 00-00 00 00 00-00 00 00 00
Questo e' l'header. La prima dword puntera' alla Tabella dei nomi. Riordinando i byte e aggiungendo ImageBase (in questo caso 0x00020000) avremo il suo indirizzo: 0x2507C Se andiamo a questo indirizzo potremo vedere le seguenti righe...
.00025070: C2 51 00 00-AE 51 00 00-00 00 00 00-52 51 00 00 -Q «Q RQ
.00025080: 44 51 00 00-24 51 00 00-34 51 00 00-0A 51 00 00 DQ $Q 4Q Q
.00025090: 02 51 00 00-F4 50 00 00-18 51 00 00-74 51 00 00 .Q ¶P .Q tQ
Questo e' l'inizio della tabella dei nomi. Il primo elemento ad esempio (a 0x2507C) punta a 0x25152. Andando a questo indirizzo troveremo...
.00025150: 00 00 25 02-5F 65 78 63-65 70 74 5F-68 61 6E 64 %._except_hand
.00025160: 6C 65 72 33-00 00 4D 53-56 43 52 54-32 30 2E 64 ler3 MSVCRT20.d
Il nome della funzione!
Se riguardate l'header dovreste anche vedere dove inizia la tabella degli indirizzi (0x250C0). A RunTime l'indirizzo di _except_handler3 vi verra' caricato proprio a 0x250C0. Cioe' l'indirizzo della prima funzione nel primo elemento, quello della seconda nel secondo elemento e cosi' via.
N.B. Tutti gli indirizzi sono RVA e ad essi va sommato ImageBase. Questo porta a qualche calcolo in piu', ma ci garantisce una perfetta rilocabilita' dell'header.
-In Pratica
Facciamo finta di voler usare ShellExecuteA in un prog che non linka shell32 (es. HyperTerminal)
1) Leggete la dword a 0x100. Questo e' l'inizio dell'header di import. Portatevi a tale indirizzo: 0x25000 (ricordatevi di sommare ImageBase). Cominciate a copiare tutto quello che c'e', a blocchi di 20 byte, in una zona vuota del file (o, se vi piace di piu', in un nuovo segmento che vi sarete creati). Quando incontrate una entry che ha l'ImportName nullo fermatevi, perche' l'header e' finito (in questo caso ci sono solo 3 entries).
In C sarebbe:
---------------------------------------------------------------------------
//Trasforma l'RVA in indirizzo fisico
DWORD Where=ImportOffS-ImRVA+ImAdd;
fseek (ffileS,Where, SEEK_SET);
fseek (ffileD,BlankSpace, SEEK_SET);
for(;;)
{
fread(buff,1,20,ffileS);
// Se trova una entry nulla finisce
if ( *((DWORD *)&buff[12])==0) break;
fwrite(buff,1,20,ffileD);
}
dove
ImportOffS e' l'indirizzo a 0x100.
ImRVA e' l'RVA del segmento di import (lo trovate nell'header dei segmenti, o con lo HIEW premendo F8 e poi F6); in questo caso vale 0x5000
ImAdd e' l'indirizzo fisico del segmento di import; in questo caso vale 0xA00.
BlankSpace e' una zona vuota del file.
ffileS e ffileD rappresentano il file originale e il file modificato.
----------------------------------------------------------------------------
2) Rimpiazzate l'header con le informazioni che vi servono.
Es:
----------------------------------------------------------------------------
DWORD Where=ImportOffS-ImRVA+ImAdd; // Sovrascriviamo
fseek (ffileD,Where, SEEK_SET); // il vecchio header
fprintf(ffileD,"SHELL32.DLL"); fputc(0x00,ffileD); // Nome del modulo
// da caricare
fputc(0x72,ffileD); // Id per la funzione
fputc(0x00,ffileD); // " " " "
fprintf(ffileD,"ShellExecuteA");fputc(0x00,ffileD); // Nome della funzione
DWORD Stuff=ImportOffS+12; // Puntatore alla stringa
fwrite(&Stuff,4,1,ffileD); // "ShellExecuteA" con l'Id
fputc(0x00,ffileD); // Per marcare la fine della NameTable
fputc(0x00,ffileD);
fputc(0x00,ffileD);
fputc(0x00,ffileD);
fputc(0x00,ffileD); // AddressTable
fputc(0x00,ffileD);
fputc(0x00,ffileD);
fputc(0x00,ffileD);
fputc(0x00,ffileD);
fputc(0x00,ffileD);
fputc(0x00,ffileD);
fputc(0x00,ffileD);
-----------------------------------------------------------------------------
A questo punto, al posto del vecchio header, avremo il nome della DLL (a ImportOffs+0), il nome della funzione che ci serve (a ImportOffs+12) e il puntatore (a ImportOffs+28) alla stringa a ImportOffs+12. Quest'ultimo, in realta', rappresenta la tabella dei nomi con un unico elemento (cioe' il puntatore alla stringa ShellExecute) e che termina con una entry nulla:
.00025000: 53 48 45 4C-4C 33 32 2E-44 4C 4C 00-72 00 53 68 SHELL32.DLL r Sh
.00025010: 65 6C 6C 45-78 65 63 75-74 65 41 00-0C 50 00 00 ellExecuteA .P
.00025020: 00 00 00 00-00 00 00 00-00 00 00 00-00 00 00 00
N.B. Per sapere qual'e' l'Id della funzione vi conviene cercarlo in un altro prog che la usi (di ShellExecuteA e' 0x7200)!
3) Aggiungete una entry all'header spostato. Riposizionatevi alla fine dell'header copiato e partite!
Es:
--------------------------------------------------------------
Stuff=ImportOffS+28;
fwrite(&Stuff,4,1,ffileD); // Puntatore alla NameTable
Stuff=0x00000000;
fwrite(&Stuff,4,1,ffileD); // Vuoti
fwrite(&Stuff,4,1,ffileD);
fwrite(&ImportOffS,4,1,ffileD); // Puntatore al nome della libreria
Stuff=ImportOffS+36; // Puntatore alla AddressTable
fwrite(&Stuff,4,1,ffileD);
---------------------------------------------------------------
Quindi, il nostro header adesso contiene una nuova entry composta cosi'
DD - Puntatore alla NameTable (che ha un unico elemento che punta alla stringa Id+"ShellExecuteA")
DD - Vuoto
DD - Vuoto
DD - Puntatore alla stringa "shell32.dll" (cioe' il nome della libreria)
DD - Puntatore alla AddressTable (cioe' l'offset che a RunTime conterra' l'indirizzo di ShellExecuteA)
N.B. Ricordatevi di aggiungere una entry (20 byte) nulla per chiudere l'header.
Avrete (supponendo che il nosto BlankSpace inizi a 0x257A0) :
-vecchio header spostato
.000257A0: 7C 50 00 00-80 4F 50 2A-FF FF FF FF-66 51 00 00 |P ÇOP* fQ
.000257B0: C0 50 00 00-64 50 00 00-9E 4F 50 2A-FF FF FF FF +P dP ×OP*
.000257C0: A0 51 00 00-A8 50 00 00-70 50 00 00-A0 1C 0C 32 áQ ¿P pP á..2
.000257D0: FF FF FF FF-D4 51 00 00-B4 50 00 00-00 00 00 00 ÈQ ¦P
.000257E0: 00 00 00 00-00 00 00 00-00 00 00 00-00 00 00 00
-nuova entry
.000270F0: 1C 50 00 00-00 00 00 00-00 00 00 00-00 50 00 00 .P P
.00027100: 24 50 00 00-00 00 00 00-00 00 00 00-00 00 00 00 $P
.00027110: 00 00 00 00-00 00 00 00-00 00 00 00-00 00 00 00
.00027120: 00 00 00 00-00 00 00 00-00 00 00 00-00 00 00 00
4) Modificate il puntatore all'header e la sua dimensione.
Andate all'offset 0x100 e fatelo puntare al vostro nuovo header L'indirizzo deve essere RVA (senza pero' ImageBase). Per ottenere l'RVA prendete l'offset fisico del vostro header (BlankSpace), sottraeteci l'offset fisico del segmento dove si trova, e sommateci l'RVA (sempre del segmento dove si trova).
Poi spostatevi all'offset 0x104 e sommate 0x14 al valore che trovate. In realta' state ingrandeno l'header di 20 byte (cioe' la dimensione dell'entry che avete aggiunto). Questa operazione sembra non servire a niente, ma non si sa mai.
5) Richiamate ShellExecuteA.
A RunTime la dword a ImportOffS+36 (cioe' a 0x25024) verra' riempita con l'indirizzo di ShellExecuteA, quindi vi bastera' fare una call reindirizzata a ImportOffS+36. Infatti ora shell32 sara' mappata nel vostro spazio di indirizzamento!!!!
Per callare basta inserire, dove vi serve, il codice
FF 15 24 50 02 00
^^call^^ ^^indirizzo^^
per richiamare ShellExecuteA =)
Visto che non era tanto difficile?
N.B. FF15 fa una call all'indirizzo contenuto nell'offset che riceve come parametro.
I RINGRAZIAMENTI
Ho fatto praticamente tutto da solo, quindi l'unico che mi sento di ringraziare veramente e' il mio vecchio compagno di coding-hacking-cracking R@Nc0r.
I SALUTI
Saluto tutti i miei amici ed in particolar modo |LnZ| e ALoR (RingZer0), 10t80r (NeuroZone2) e PeSy (Casa mia). Inoltre saluto tutti i membri di RingZer0, che spero di conoscere al prossimo HackIt. Non saluto i bastardi che fanno i tutorials sbagliati, i miei professori dell'uni, le tipe che se la tirano.
LA MASSIMA
Non so se, quando cambieranno, le cose saranno migliori....ma so che, se voglio che siano migliori, devono cambiare.
LA MINIMA
Durante la notte 12 gradi centigradi nella zona di MilanoLambrate.
(la signature la dovete guardare con l'Edit di MS-DOS, altrimenti si vede la seguente schifezza...)
° ± ² ± ° ° ± ² ± °
°±±±²±±±° °±±±²±±±°
\°°°±°°°/ \°°°±°°°/
°±±²±±° °±±²±±°
°±±²±±° | | / °±±²±±°
°±±²±±° | \ | / _\ °±±²±±°
°±±²±±° |_ \_ aG °±±²±±°
_°±±²±±°_ __°±±²±±°_