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
.