In sostanza, devi controllare l'ambiente di esecuzione delle app. Non c'è niente di magico in questo. Un paio di soluzioni che mi vengono in mente:
-
Potresti in qualche modo 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 il collegamento a un processo setuid/setgid. Verifica però se lo fa per setuid non di proprietà di root!
-
Potresti utilizzare un caricatore sicuro per eseguire le tue app invece di ld, che rifiuta di riconoscere LD_PRELOAD. Ciò potrebbe interrompere alcune app esistenti. Guarda il lavoro di Mathias Payer per saperne di più, anche se dubito che ci sia uno strumento standard che puoi semplicemente applicare.
-
Puoi 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 al momento non riesco a trovare informazioni su come.
-
Infine, puoi eseguire il sandbox delle tue app e impedire alle app di avviare direttamente altri processi con un ambiente personalizzato o di modificare la home directory dell'utente. Non esiste nemmeno uno strumento già pronto per questo (c'è molto lavoro in corso e nulla è ancora implementabile).
Probabilmente ci sono limiti alle soluzioni di cui sopra e ad altre soluzioni candidate a seconda delle app che devi eseguire, chi sono gli utenti e qual è il modello di minaccia. Se puoi rendere la tua domanda più precisa, cercherò di migliorare la risposta di conseguenza.
Modifica: tieni presente che un utente malintenzionato può modificare solo il proprio ambiente di esecuzione (a meno che non possa aumentare i privilegi a root con qualche exploit ma poi hai altri problemi da gestire). Quindi, un utente di solito non usa le iniezioni LD_PRELOAD perché può già eseguire codice con gli stessi privilegi. Gli attacchi hanno senso per alcuni scenari:
- infrangere i controlli relativi alla sicurezza sul lato client del software client-server (in genere trucchi nei videogiochi o fare in modo che un'app client ignori alcuni passaggi di convalida con il server del suo distributore)
- installazione di malware permanente quando prendi il controllo della sessione o del processo di un utente (o perché ha dimenticato di disconnettersi e hai accesso fisico al dispositivo o perché hai sfruttato una delle sue app con contenuti creati)
La maggior parte dei punti di Steve DL sono buoni, l'approccio "migliore" è utilizzare un linker di runtime (RTLD) su cui hai un maggiore controllo. Il file "LD_
" le variabili sono hardcoded in glibc (iniziano con elf/rtld.c
). Il glibc RTLD ha molte "caratteristiche", e anche lo stesso ELF ha alcune sorprese con le sue voci DT_RPATH e DT_RUNPATH, e $ORIGIN
(vedi https://unix.stackexchange.com/questions/22926/where-do-executables-look-for-shared-objects-at-runtime).
Normalmente se si desidera impedire (o modificare) determinate operazioni quando non è possibile utilizzare autorizzazioni normali o una shell limitata, è invece possibile forzare il caricamento di una libreria per eseguire il wrapping delle chiamate libc:questo è esattamente il trucco utilizzato dal malware e ciò significa è difficile usare la stessa tecnica contro di essa.
Un'opzione che ti consente di agganciare l'RTLD in azione è l'audit caratteristica, per usarla devi impostare LD_AUDIT
per caricare un oggetto condiviso (contenente l'API di controllo definita denominata functions). Il vantaggio è che puoi agganciare le singole librerie che vengono caricate, lo svantaggio è che è controllato con una variabile d'ambiente...
Un trucco meno usato è un altro dei ld.so
"caratteristiche":/etc/ld.so.preload
. Quello 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 sono riportati alcuni sperimentali code per fare questo, dovresti probabilmente pensarci bene prima di usarlo in produzione, ma mostra che può essere fatto.
#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]='\0'; }
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
:
$ 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
(sì, ha interrotto il processo perché si è rilevato, poiché quel percorso non è "affidabile".)
Il modo in cui funziona è:
- usa una funzione chiamata
_init()
per ottenere il controllo prima dell'inizio del processo (un punto sottile è che funziona perchéld.so.preload
le startup vengono invocate prima di quelle qualsiasiLD_PRELOAD
librerie, anche se non riesco a trovarlo documentato ) - usa
dl_iterate_phdr()
per iterare su tutti gli oggetti dinamici in questo processo (più o meno equivalente a rovistare in/proc/self/maps
) - risolvi tutti i percorsi e confrontali con un elenco codificato di prefissi attendibili
- troverà tutte le librerie caricate all'avvio del processo, anche quelle trovate tramite
LD_LIBRARY_PATH
, ma no quelli successivamente caricati condlopen()
.
Questo ha un semplice geteuid()>100
condizioni per minimizzare i problemi. Non si fida di RPATHS né li gestisce separatamente in alcun modo, quindi questo approccio necessita di alcune regolazioni per tali binari. Potresti invece banalmente modificare il codice di interruzione per accedere tramite syslog.
Se modifichi /etc/ld.so.preload
e se sbagli qualcosa, potresti danneggiare seriamente il tuo sistema . (Hai una shell di salvataggio collegata staticamente, vero?)
Potresti utilmente testare in modo controllato usando unshare
e mount --bind
per limitarne l'effetto (cioè avere un /etc/ld.so.preload
privato ). Hai bisogno di root (o CAP_SYS_ADMIN
) per unshare
però:
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"
Se i tuoi utenti accedono tramite ssh, allora ForceCommand
di OpenSSH e Match group
potrebbe probabilmente essere utilizzato, o uno script di avvio su misura per un demone sshd dedicato "utente non fidato".
Per riassumere:l'unico modo in cui puoi fare esattamente ciò che richiedi (prevenire LD_PRELOAD) è utilizzare un linker di runtime compromesso o più configurabile. Sopra c'è una soluzione alternativa che ti consente di limitare le librerie in base a un percorso attendibile, eliminando il problema di questo malware furtivo.
Come ultima risorsa potresti costringere gli utenti a utilizzare sudo
per eseguire tutti i programmi, questo ripulirà bene il loro ambiente e, poiché è setuid, non ne risentirà. Solo un'idea;-) A proposito di sudo
, utilizza lo stesso trucco della libreria per impedire ai programmi di fornire agli utenti una shell backdoor con il suo NOEXEC
caratteristica.
Sì, c'è un modo:non lasciare che quell'utente esegua codice arbitrario. Dai loro una shell ristretta, o meglio, solo un insieme predefinito di comandi.
Non impediresti l'esecuzione di alcun malware, a meno che tu non abbia utilizzato un meccanismo di escalation dei privilegi non standard che non cancella queste variabili. I normali meccanismi di escalation dei privilegi (eseguibili setuid, setgid o setcap; 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 eseguibili installati e farli comportare in modo diverso. Un grosso problema:l'utente può eseguire i propri eseguibili (compresi quelli collegati staticamente). Tutto ciò che otterresti è un po' di responsabilità se registri tutti i execve
chiamate. Ma se fai affidamento su questo per rilevare malware, c'è così tanto che può sfuggire alla tua sorveglianza che non mi preoccuperei. Molti linguaggi di programmazione offrono funzionalità simili a LD_LIBRARY_PATH
:CLASSPATH
, PERLLIB
, PYTHONPATH
, ecc. Non li inserirai tutti nella blacklist, solo un approccio whitelist sarebbe utile.
Come minimo, dovresti bloccare ptrace
anche:con ptrace
, qualsiasi eseguibile può essere fatto per eseguire qualsiasi codice. Blocco ptrace
può essere una buona idea, ma soprattutto perché sono state trovate così tante vulnerabilità attorno ad essa che è probabile che alcune non vengano scoperte.
Con una shell ristretta, il LD_*
le variabili sono in realtà una preoccupazione, perché l'utente può eseguire solo una serie di programmi pre-approvati e LD_*
consente loro di aggirare questa restrizione. Alcune shell ristrette consentono di rendere le variabili di sola lettura.