C'è un modo per bloccare LD_PRELOAD e LD_LIBRARY_PATH su Linux?

7

Preferibilmente con un kernel Linux vanilla che non usa SELinux o GrSecurity?

Ultimamente c'è stato un caso di malware server UNIX selvaggio:

link

che sfrutta LD_PRELOAD per implementare un trojan userspace nascosto. Sto pensando che sarebbe meglio impedire agli utenti limitati di fare confusione con i percorsi della libreria condivisa, se possibile.

(Vorrei andare oltre, ad esempio GrSecurity / trusted path execution, ma al momento non è pratico.)

C'è un modo per farlo in modo affidabile?

Modifica: quello che intendevo sopra è che, normalmente, qualsiasi utente può utilizzare LD_PRELOAD per iniettare qualsiasi libreria in qualsiasi eseguibile che può essere eseguito e che non è setuid o setgid. Questo è un meccanismo noto per la creazione di malware nascosto e persistente. C'è un modo per impedire a un determinato utente di utilizzare LD_PRELOAD affatto ?

    
posta DanL4096 21.07.2014 - 18:30
fonte

3 risposte

10

Essenzialmente, è necessario controllare l'ambiente di esecuzione delle app. Non c'è magia a riguardo. Un paio di soluzioni che vengono in mente:

  1. In qualche modo potresti impostare tutti i binari che ti preoccupano come setuid / setgid (ciò non significa che debbano essere di proprietà di root, per quanto ne so). Linux normalmente impedisce l'associazione a un processo setuid / setgid . Si prega di verificare se lo fa per setuid non-root però!

  2. Potresti usare un caricatore sicuro per eseguire le tue app invece di ld, che rifiuta di riconoscere LD_PRELOADs. Questo potrebbe interrompere alcune app esistenti. Per ulteriori informazioni, consulta il lavoro di Mathias Payer , anche se dubito che ci sia uno strumento disponibile che puoi semplicemente applicare.

  3. Potresti ricostruire i tuoi binari con una libc che disabilita LD_PRELOAD e dlsym. Ho sentito che musl può farlo se passa le opzioni giuste, ma non può ripristinare le informazioni su come adesso.

  4. Infine, è possibile eseguire il sandbox delle app e impedire alle app di avviare direttamente altri processi con un ambiente personalizzato o modificare la directory home dell'utente. Non c'è nemmeno uno strumento pronto per questo (è molto lavoro in corso e nulla è ancora dispiegabile).

Probabilmente esistono dei limiti alle soluzioni di cui sopra e ad altre soluzioni candidate, a seconda delle app da eseguire, degli utenti e del modello di minaccia. Se riesci a rendere la tua domanda più precisa, proverò a migliorare la risposta di conseguenza.

Modifica: tieni presente che un utente malintenzionato può modificare solo il proprio ambiente di esecuzione (a meno che non sia in grado di eseguire l'escalation dei privilegi per eseguire il root con qualche exploit, ma di avere altri problemi da gestire). Pertanto, un utente di solito non utilizza le iniezioni LD_PRELOAD perché possono già eseguire codice con gli stessi privilegi. Gli attacchi hanno senso per alcuni scenari:

  • interruzione dei controlli relativi alla sicurezza sul lato client del software client-server (in genere trucchi nei videogiochi o esecuzione di un'app client per bypassare qualche passo di convalida con il server del proprio distributore)
  • installazione di malware permanente quando si acquisisce la sessione o il processo di un utente (perché si è dimenticato di disconnettersi e si dispone dell'accesso fisico al dispositivo o perché si è sfruttata una delle proprie app con contenuto creato)
risposta data 21.07.2014 - 18:59
fonte
7

La maggior parte dei punti di Steve DL è buona, l'approccio "migliore" è quello di utilizzare un linker di runtime (RTLD) su cui si ha maggiore controllo. Le variabili " LD_ " sono codificate in glibc (inizia con elf/rtld.c ). L'RTLD glibc ha molte "caratteristiche", e anche l'ELF stesso ha alcune sorprese con le sue voci DT_RPATH e DT_RUNPATH e $ORIGIN (vedi link ).

Normalmente se vuoi impedire (o alterare) certe operazioni quando non puoi usare normali permessi o una shell ristretta, puoi forzare il caricamento di una libreria per avvolgere le chiamate libc: questo è esattamente il trucco che il malware sta usando, e questo significa che è difficile usare la stessa tecnica contro di esso.

Un'opzione che ti permette di collegare l'RTLD in azione è la verifica funzione , per utilizzare questo si imposta LD_AUDIT per caricare un oggetto condiviso (contenente l'API di controllo definita denominata funzioni). Il vantaggio è che si arriva ad agganciare le singole librerie che vengono caricate, l'inconveniente è che è controllato con una variabile di ambiente ...

Un trucco meno utilizzato è un altro delle "caratteristiche" ld.so : /etc/ld.so.preload . Ciò che puoi fare con questo è caricare il tuo codice in ogni processo dinamico, il vantaggio è che è controllato da un file limitato, gli utenti non-root non possono modificarlo o sovrascriverlo (entro limiti ragionevoli, ad esempio se gli utenti possono installare la propria toolchain o trucchi simili).

Di seguito è riportato un codice experimental per fare ciò, dovresti probabilmente riflettere seriamente prima di utilizzarlo in produzione, ma mostra che è possibile farlo.

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <dlfcn.h>
#include <link.h>
#include <assert.h>
#include <errno.h>

int dlcb(struct dl_phdr_info *info, size_t size, void *data);

#define DEBUG 1
#define dfprintf(fmt, ...) \
    do { if (DEBUG) fprintf(stderr, "[%5i %14s#%04d:%8s()] " fmt, \
          getpid(),__FILE__, __LINE__, __func__, __VA_ARGS__); } while (0)

void _init()
{
    char **ep,**p_progname;
    int dlcount[2]={0,0};

    dfprintf("ldwrap2 invoked!\n","");

    p_progname=dlsym(RTLD_NEXT, "__progname"); 
    dfprintf("__progname=<%s>\n",*p_progname);

    // invoke dlcb callback for every loaded shared object
    dl_iterate_phdr(dlcb,dlcount);

    dfprintf("good count %i, bad count %i\n",dlcount[0],dlcount[1]);

    if ((geteuid()>100) && dlcount[1]) {
        for (ep=environ; *ep!=NULL; ep++)
            if (!strncmp(*ep,"LD_",3))
                fprintf(stderr,"%s\n", *ep);
        fprintf(stderr,"Terminating program: %s\n",*p_progname);
        assert_perror(EPERM);
    }
    dfprintf("on with the show!\n","");
}

int dlcb(struct dl_phdr_info *info, size_t size, void *data)
{
    char *trusted[]={"/lib/", "/lib64/",
                     "/usr/lib","/usr/lib64",
                     "/usr/local/lib/",
                     NULL};
    char respath[PATH_MAX+1];
    int *dlcount=data,nn;

    if (!realpath(info->dlpi_name,respath)) { respath[0]='
$ LD_PRELOAD=./ldwrap2.so ls
Unexpected DSO loaded from /home/mr/code/C/ldso/ldwrap2.so
LD_PRELOAD=./ldwrap2.so
Terminating program: ls
ls: ldwrap2.c:47: _init: Unexpected error: Operation not permitted.
Aborted
'; } dfprintf("name=%s (%s)\n", info->dlpi_name, respath); // special case [stack] and [vdso] which have no filename if (respath && strlen(respath)) { for (nn=0; trusted[nn];nn++) { dfprintf("strncmp(%s,%s,%i)\n", trusted[nn],respath,strlen(trusted[nn])); if (!strncmp(trusted[nn],respath,strlen(trusted[nn]))) { dlcount[0]++; break; } } if (trusted[nn]==NULL) { dlcount[1]++; fprintf(stderr,"Unexpected DSO loaded from %s\n",respath); } } return 0; }

Compila con gcc -nostartfiles -shared -Wl,-soname,ldwrap2.so -ldl -o ldwrap2 ldwrap2.c . Puoi testarlo con LD_PRELOAD senza modificare /etc/ld.so.conf :

echo "/usr/local/lib/ldwrap2.so" > /etc/ld.so.conf.test
unshare -m -- sh -c "mount --bind /etc/ld.so.preload.test /etc/ld.so.preload; /bin/bash"

(sì, ha smesso di arrestare il processo perché ha rilevato se stesso, poiché quel percorso non è "attendibile".)

Il modo in cui funziona è:

  • usa una funzione chiamata _init() per ottenere il controllo prima dell'inizio del processo (un punto sottile è che questo funziona perché ld.so.preload startup vengono invocati prima di quelle qualsiasi LD_PRELOAD librerie, anche se non riesco a trovare questo documentato )
  • usa dl_iterate_phdr() per eseguire un'iterazione su tutti gli oggetti dinamici in questo processo (approssimativamente equivalente a rovistare in /proc/self/maps )
  • risolva tutti i percorsi e confronta con un elenco codificato di prefissi attendibili
  • troverà tutte le librerie caricate all'avvio del processo, anche quelle trovate tramite LD_LIBRARY_PATH , ma non quelle successivamente caricate con dlopen() .

Questo ha una semplice condizione geteuid()>100 per minimizzare i problemi. Non si fida di RPATHS o gestisce quelli separatamente in alcun modo, quindi questo approccio richiede un po 'di ottimizzazione per questi binari. Invece potresti modificare banalmente il codice di interruzione per accedere tramite syslog.

Se modifichi /etc/ld.so.preload e ottieni una risposta errata, potresti rompere gravemente il tuo sistema . (Hai una shell di salvataggio collegata in modo statico, giusto?)

Potresti testare utilmente in modo controllato usando unshare e mount --bind per limitare il suo effetto (cioè avere un /etc/ld.so.preload privato). Hai bisogno di root (o CAP_SYS_ADMIN ) per unshare però:

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <dlfcn.h>
#include <link.h>
#include <assert.h>
#include <errno.h>

int dlcb(struct dl_phdr_info *info, size_t size, void *data);

#define DEBUG 1
#define dfprintf(fmt, ...) \
    do { if (DEBUG) fprintf(stderr, "[%5i %14s#%04d:%8s()] " fmt, \
          getpid(),__FILE__, __LINE__, __func__, __VA_ARGS__); } while (0)

void _init()
{
    char **ep,**p_progname;
    int dlcount[2]={0,0};

    dfprintf("ldwrap2 invoked!\n","");

    p_progname=dlsym(RTLD_NEXT, "__progname"); 
    dfprintf("__progname=<%s>\n",*p_progname);

    // invoke dlcb callback for every loaded shared object
    dl_iterate_phdr(dlcb,dlcount);

    dfprintf("good count %i, bad count %i\n",dlcount[0],dlcount[1]);

    if ((geteuid()>100) && dlcount[1]) {
        for (ep=environ; *ep!=NULL; ep++)
            if (!strncmp(*ep,"LD_",3))
                fprintf(stderr,"%s\n", *ep);
        fprintf(stderr,"Terminating program: %s\n",*p_progname);
        assert_perror(EPERM);
    }
    dfprintf("on with the show!\n","");
}

int dlcb(struct dl_phdr_info *info, size_t size, void *data)
{
    char *trusted[]={"/lib/", "/lib64/",
                     "/usr/lib","/usr/lib64",
                     "/usr/local/lib/",
                     NULL};
    char respath[PATH_MAX+1];
    int *dlcount=data,nn;

    if (!realpath(info->dlpi_name,respath)) { respath[0]='
$ LD_PRELOAD=./ldwrap2.so ls
Unexpected DSO loaded from /home/mr/code/C/ldso/ldwrap2.so
LD_PRELOAD=./ldwrap2.so
Terminating program: ls
ls: ldwrap2.c:47: _init: Unexpected error: Operation not permitted.
Aborted
'; } dfprintf("name=%s (%s)\n", info->dlpi_name, respath); // special case [stack] and [vdso] which have no filename if (respath && strlen(respath)) { for (nn=0; trusted[nn];nn++) { dfprintf("strncmp(%s,%s,%i)\n", trusted[nn],respath,strlen(trusted[nn])); if (!strncmp(trusted[nn],respath,strlen(trusted[nn]))) { dlcount[0]++; break; } } if (trusted[nn]==NULL) { dlcount[1]++; fprintf(stderr,"Unexpected DSO loaded from %s\n",respath); } } return 0; }

Se i tuoi utenti accedono tramite ssh, probabilmente potrebbero essere usati ForceCommand di OpenSSH e Match group , oppure uno script di avvio su misura per un demone sshd dedicato "utente non sicuro".

Riassumendo: l'unico modo per fare esattamente ciò che si richiede (impedire LD_PRELOAD) consiste nell'utilizzare un linker di runtime compromesso o più configurabile. Qui sopra c'è una soluzione alternativa che ti consente di limitare le librerie in base al percorso attendibile, che elimina il rischio di malware di tipo furtivo.

Come ultima risorsa potresti costringere gli utenti a usare sudo per eseguire tutti i programmi, questo pulirà bene il loro ambiente e, dato che è setuid, non ne risentirà. Solo un'idea ;-) A proposito di sudo , usa lo stesso trucco della libreria per impedire che i programmi utenti una shell backdoor con la sua funzione NOEXEC .

    
risposta data 23.07.2014 - 00:47
fonte
1

Sì, c'è un modo: non lasciare che quell'utente esegua codice arbitrario. Dagli una shell ristretta, o meglio, solo un insieme predefinito di comandi.

Non impedirai l'esecuzione di malware, a meno che tu non abbia usato un meccanismo di escalation dei privilegi non standard che non cancelli queste variabili. I normali meccanismi di escalation dei privilegi (setuid, setgid o setcap eseguibili, chiamate tra processi) ignorano queste variabili. Quindi non si tratta di prevenire il malware, si tratta solo di rilevare il malware.

LD_PRELOAD e LD_LIBRARY_PATH consente a un utente di eseguire file eseguibili installati e di comportarsi diversamente. Grande affare: l'utente può eseguire i propri eseguibili, (compresi quelli collegati staticamente). Tutto quello che otterresti è un po 'di responsabilità se stai registrando tutte le chiamate execve . Ma se ti affidi a questo per rilevare il malware, c'è così tanto che può sfuggire alla tua sorveglianza che non mi preoccuperei. Molti linguaggi di programmazione offrono servizi simili a LD_LIBRARY_PATH : CLASSPATH , PERLLIB , PYTHONPATH , ecc. Non hai intenzione di inserirli tutti nella lista nera, solo un approccio alla lista bianca sarebbe utile.

Per lo meno, dovresti bloccare anche ptrace : con ptrace , è possibile eseguire qualsiasi eseguibile per eseguire qualsiasi codice. Il blocco di ptrace può essere una buona idea, ma soprattutto perché sono state scoperte così tante vulnerabilità che è probabile che alcune siano ancora da scoprire.

Con una shell limitata, le variabili LD_* sono in realtà un problema, poiché l'utente può solo eseguire un set di programmi pre-approvato e LD_* consente loro di aggirare questa restrizione. Alcune shell limitate consentono di creare variabili di sola lettura.

    
risposta data 22.07.2014 - 17:15
fonte

Leggi altre domande sui tag