Ti sei mai chiesto come i malware riescano a modificare il comportamento di un programma su un sistema Linux? In questo articolo, esamineremo una delle tecniche più diffuse utilizzate proprio per raggiungere questo scopo: la manipolazione della variabile d’ambiente LD_PRELOAD. Ti guideremo passo dopo passo nelle viscere di questa tecnica.
Che cos’è LD_PRELOAD?
LD_PRELOAD è una variabile d’ambiente nei sistemi operativi basati su Linux, che permette di caricare librerie condivise personalizzate prima di qualsiasi altra libreria di sistema. Questo meccanismo consente di sovrascrivere funzioni delle librerie standard, rendendolo uno strumento potente per sviluppatori e amministratori di sistema per scopi come il debug e il testing. Tuttavia, questa funzionalità può essere anche sfruttata dai malware per ottenere persistenza o alterare il comportamento di un programma, manipolando le funzioni critiche del sistema.
Come il Sistema Linux Carica e Utilizza le Librerie Condivise
Quando un programma su un sistema Linux necessita di una libreria condivisa, il processo di caricamento viene gestito dal dynamic linker (come ld.so o ld-linux.so). Questo processo segue una serie di passaggi.
Durante la compilazione, le librerie condivise vengono referenziate ma non incluse direttamente nel binario; il programma conserva solo i riferimenti a queste librerie, che saranno risolti durante l’esecuzione.
All’avvio del programma, il dynamic linker si occupa di trovare e caricare le librerie necessarie. La ricerca segue un ordine preciso: prima vengono consultati i percorsi indicati dalla variabile LD_PRELOAD, poi quelli definiti nel binario stesso tramite l’opzione -rpath, seguiti dalla variabile LD_LIBRARY_PATH.
Esempio di Compilazione con -rpath
Supponiamo di avere una libreria personalizzata situata in /custom/lib e di voler garantire che il programma utilizzi questa libreria senza dipendere da variabili d’ambiente. Possiamo specificare il percorso della libreria direttamente durante la fase di compilazione con l’opzione -rpath. Ecco un esempio di comando di compilazione:
gcc -o my_program my_program.c -L/custom/lib -Wl,-rpath,/custom/lib
-L/custom/lib: Specifica la directory dove si trovano le librerie personalizzate.-Wl,-rpath,/custom/lib: Informa il linker che deve includere il percorso/custom/libdirettamente nel binario come percorso di ricerca per le librerie condivise.
In questo modo, anche se LD_LIBRARY_PATH o altre variabili d’ambiente non sono impostate, il dynamic linker cercherà la libreria condivisa direttamente in /custom/lib quando il programma verrà eseguito.
Successivamente, il linker verifica il file /etc/ld.so.preload, la cache delle librerie (/etc/ld.so.cache), e infine i percorsi standard di sistema come /lib e /usr/lib.
Una volta trovata la libreria, il linker la carica in memoria e collega le funzioni e le risorse richieste dal programma. Se la libreria ha ulteriori dipendenze, queste vengono caricate e collegate a loro volta. Infine, il programma può proseguire con la sua esecuzione. Se una libreria non viene trovata, il processo termina con un messaggio di errore.
Per assicurarsi che le librerie siano trovate facilmente, è possibile aggiornare la cache delle librerie con il comando ldconfig, utile dopo l’installazione di nuove librerie o modifiche ai percorsi esistenti. Questo meccanismo garantisce che il sistema possa gestire correttamente le librerie condivise necessarie per il funzionamento dei programmi.
Sommario dei percorsi di ricerca:
- LD_PRELOAD (variabile d’ambiente)
- Percorsi specificati nel binario (
-rpath) - LD_LIBRARY_PATH (variabile d’ambiente)
- /etc/ld.so.preload (file di pre-caricamento)
- Cache delle librerie (
/etc/ld.so.cache) - Percorsi di sistema standard (
/lib,/usr/lib, ecc.)
Routine di Inizializzazione e Terminazione nelle Librerie Condivise
Le librerie condivise su Linux possono includere routine di inizializzazione che vengono eseguite prima dell’inizio del programma principale. Una di queste è la funzione _init(), la quale, se definita, viene chiamata subito dopo il caricamento della libreria in memoria, ma prima che il programma inizi la sua esecuzione. Anche se opzionale, _init() può essere utile per configurazioni iniziali.
Oltre a _init(), le librerie possono dichiarare funzioni con l’attributo __attribute__((constructor)). Queste funzioni vengono eseguite automaticamente all’inizio del processo, precedendo l’invocazione del main(). Ciò consente di predisporre risorse o eseguire configurazioni necessarie per la libreria.
Una volta caricate tutte le librerie e completate le routine di inizializzazione, il dynamic linker cede il controllo al programma principale.
Alla fine dell’esecuzione del programma, il sistema invoca eventuali routine di terminazione delle librerie. Tra queste, _fini() esegue la pulizia finale, similmente a _init(), ma al momento della chiusura del programma. In alternativa, possono essere definite funzioni con l’attributo __attribute__((destructor)), eseguite prima che il programma termini, per liberare risorse o eseguire operazioni di pulizia necessarie.
Riepilogo delle Funzioni Chiave:
_init(): Eseguita dopo il caricamento della libreria._attribute__((constructor)): Funzioni chiamate prima delmain().main(): Punto di ingresso del programma.fini(): Eseguita alla chiusura del programma.__attribute__((destructor)): Funzioni chiamate prima della terminazione del programma.
Questi passaggi consentono al sistema Linux di caricare e gestire in modo dinamico le librerie condivise, ottimizzando l’uso della memoria e permettendo la modularità e la riusabilità del codice.
Passaggi Utilizzati dai Malware per Alterare il Comportamento di un Programma su Sistemi Linux
I malware che mirano a modificare il comportamento di un programma su Linux seguono una serie di step strategici per raggiungere il loro obiettivo. Ecco un’analisi approfondita del processo:
- Identificazione del Programma da Manipolare
Il primo passo consiste nell’individuare il programma target. Questo avviene solitamente analizzando il sistema alla ricerca di applicazioni critiche o di interesse specifico per l’attaccante. Una volta selezionato il programma, si passa alla fase successiva. - Analisi delle Librerie Condivise
Dopo aver identificato il programma, il malware esamina le librerie condivise utilizzate dall’applicazione. Questo viene fatto utilizzando strumenti come ldd o objdump, che elencano tutte le librerie condivise richieste dal programma, insieme alle funzioni offerte da ciascuna libreria.


- Selezione della Funzione da Alterare
Una volta individuate le librerie, il malware cerca una funzione specifica all’interno di queste librerie che può essere alterata per modificare il comportamento del programma. La funzione selezionata è solitamente cruciale per il funzionamento o la sicurezza del programma target. - Creazione di una Libreria Condivisa Maligna
A questo punto, viene sviluppata una libreria condivisa personalizzata che ridefinisce il comportamento della funzione selezionata. Questa libreria può ad esempio loggare informazioni sensibili, modificare i dati processati o introdurre backdoor nel sistema. - Caricamento della Libreria tramite LD_PRELOAD
L’ultimo passaggio consiste nel caricare la libreria maligna utilizzando la variabile d’ambiente LD_PRELOAD. Questa variabile forza il caricamento della libreria malevola prima di tutte le altre, permettendo così al malware di sovrascrivere le funzioni legittime con le proprie versioni alterate. Il risultato è che il programma target utilizzerà la funzione modificata senza essere consapevole del cambiamento.
Questo approccio consente al malware di manipolare il comportamento del programma in modo discreto e spesso difficile da rilevare, sfruttando le potenzialità offerte dalla dinamica gestione delle librerie su sistemi Linux.
Esempio Pratico di Alterazione del Comportamento di un Programma
In questo scenario, vedremo come modificare il comportamento di un programma durante l’esecuzione, utilizzando una libreria condivisa personalizzata. I passaggi sono i seguenti:
- Identificazione delle Funzioni da Alterare
Utilizziamo il comandoobjdump -T <programma target>per elencare tutte le funzioni che il programma richiama dalle librerie condivise. Questo ci permette di individuare la funzione specifica che vogliamo alterare. - Ricerca e Ridefinizione della Funzione
Dopo aver scelto la funzione da modificare, cerchiamo la sua firma online. La funzione viene poi riprogrammata in linguaggio C per introdurre il comportamento desiderato. - Implementazione della Libreria Condivisa
In questo esempio, alteriamo il comportamento del programma durante il caricamento, prima che la funzionemainvenga eseguita. Utilizziamo l’attributo__attribute__((constructor))per eseguire il nostro codice personalizzato all’inizio del processo. Ecco il codice del programmaexample.c:
#include <stdio.h>
#include <stdlib.h>
// Definiamo una funzione di inizializzazione personalizzata
void my_init() __attribute__((constructor));
void my_init() {
printf("Library example.so loaded!\n");
}
Questa funzione semplicemente stampa la stringa “Library example.so loaded!\n” prima che il programma inizi l’esecuzione vera e propria.
- Compilazione della Libreria
Compiliamo il codice per creare una libreria condivisa utilizzando il seguente comando:
sudo gcc -fPIC -shared -o /usr/local/lib/example.so example.c
- Caricamento della Libreria tramite LD_PRELOAD
Infine, aggiungiamo la libreriaexample.soalla variabile d’ambienteLD_PRELOADper assicurarci che venga caricata prima di qualsiasi altra libreria:
export LD_PRELOAD=/usr/local/lib/example.so
Nota: A questo punto, la funzione definita nella libreria sarà eseguita da qualsiasi programma prima che venga eseguita la funzione main.
Dopo aver completato i passaggi, eseguendo il comando ldd /bin/ls, noterai che la prima riga dell’output mostra la stringa “Library example.so loaded!\n”. Questo conferma il successo della PoC. Inoltre, è possibile osservare che il comando ls sta effettivamente caricando la libreria example.so, come evidenziato nell’elenco delle librerie condivise utilizzate. Questo dimostra come la manipolazione tramite LD_PRELOAD sia stata eseguita correttamente.


Questo esempio mostra come sia possibile alterare il comportamento di un qualsiasi programma a tempo di esecuzione utilizzando una libreria condivisa personalizzata. In questo caso, la libreria stampa un messaggio prima dell’inizio del programma principale, dimostrando il controllo esercitato durante la fase di inizializzazione.
Conclusione
In questo articolo, abbiamo esplorato come sia possibile alterare il comportamento di un programma su sistemi Linux sfruttando la variabile d’ambiente LD_PRELOAD. Attraverso una dimostrazione pratica, abbiamo visto come identificare le funzioni critiche di un programma, ridefinirle tramite una libreria condivisa personalizzata, e caricare questa libreria per manipolare il comportamento del programma durante l’esecuzione.
Questa tecnica, sebbene utile per scopi legittimi come debugging e testing, può essere anche sfruttata da malware per introdurre modifiche non autorizzate, rappresentando una minaccia alla sicurezza. L’importanza di comprendere e monitorare l’uso di LD_PRELOAD nei sistemi Linux non può essere sottovalutata, sia per proteggersi da potenziali attacchi, sia per garantire un ambiente operativo sicuro e stabile.






