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:

  • -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/lib direttamente 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:

  1. LD_PRELOAD (variabile d’ambiente)
  2. Percorsi specificati nel binario (-rpath)
  3. LD_LIBRARY_PATH (variabile d’ambiente)
  4. /etc/ld.so.preload (file di pre-caricamento)
  5. Cache delle librerie (/etc/ld.so.cache)
  6. 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:

  1. _init(): Eseguita dopo il caricamento della libreria.
  2. _attribute__((constructor)): Funzioni chiamate prima del main().
  3. main(): Punto di ingresso del programma.
  4. fini(): Eseguita alla chiusura del programma.
  5. __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:

  1. 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.
  2. 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.
Elenco delle librerie condivise del programma ls
Elenco delle funzioni e delle relative librerie  utilizzate dal comando ls

  • 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:

  1. Identificazione delle Funzioni da Alterare
    Utilizziamo il comando objdump -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.
  2. 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.
  3. Implementazione della Libreria Condivisa
    In questo esempio, alteriamo il comportamento del programma durante il caricamento, prima che la funzione main venga eseguita. Utilizziamo l’attributo __attribute__((constructor)) per eseguire il nostro codice personalizzato all’inizio del processo. Ecco il codice del programma example.c:

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:
  • Caricamento della Libreria tramite LD_PRELOAD
    Infine, aggiungiamo la libreria example.so alla variabile d’ambiente LD_PRELOAD per assicurarci che venga caricata prima di qualsiasi altra libreria:

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.

Esecuzione della PoC e del comando ldd /bin/ls
Questa immagine mostra come il comando ls e whoami siano stati alterati

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.

Ti è piaciuto l’articolo? Seguici su Linkedin per sostenerci!

consigliati