La mayoría de los puntos de Steve DL son buenos, el enfoque "mejor" es utilizar un enlazador de tiempo de ejecución (RTLD) sobre el que tenga más control. Las variables " LD_
" están codificadas en glibc (comience con elf/rtld.c
). El glibc RTLD tiene muchas "características", e incluso ELF tiene algunas sorpresas con sus entradas DT_RPATH y DT_RUNPATH , y $ORIGIN
(consulte enlace ).
Normalmente, si desea evitar (o alterar) ciertas operaciones cuando no puede usar permisos normales o un shell restringido, puede forzar la carga de una biblioteca para ajustar las llamadas libc: este es exactamente el truco que usa el malware. y esto significa que es difícil usar la misma técnica en su contra.
Una opción que le permite conectar el RTLD en acción es la audit feature , para usar esto, configura LD_AUDIT
para cargar un objeto compartido (que contiene las funciones de la API de auditoría definidas). El beneficio es que puedes enganchar las bibliotecas individuales que se están cargando, el inconveniente es que se controla con una variable de entorno ...
Un truco menos usado es uno más de las características " ld.so
": /etc/ld.so.preload
. Lo que puede hacer con esto es cargar su propio código en cada proceso dinámico, la ventaja es que está controlado por un archivo restringido, los usuarios no root no pueden modificarlo ni anularlo (dentro de lo razonable, por ejemplo, si los usuarios pueden instalar su propia cadena de herramientas o trucos similares).
A continuación hay un código experimental para hacer esto, probablemente deberías pensar mucho en esto antes de usarlo en producción, pero muestra que se puede hacer.
#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;
}
Compilar con gcc -nostartfiles -shared -Wl,-soname,ldwrap2.so -ldl -o ldwrap2 ldwrap2.c
.
Puede probar esto con LD_PRELOAD
sin modificar /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í, detuvo el proceso porque se detectó a sí mismo, ya que esa ruta no es "confiable".)
La forma en que funciona esto es:
- use una función llamada
_init()
para obtener el control antes de que comience el proceso (un punto sutil es que esto funciona porque ld.so.preload
startups se invocan antes de esas bibliotecas LD_PRELOAD
, aunque no puedo encontrar esto documentado )
- use
dl_iterate_phdr()
para iterar sobre todos los objetos dinámicos en este proceso (aproximadamente equivalente a rebuscar en /proc/self/maps
)
- resuelva todas las rutas y compare con una lista codificada de prefijos confiables
- encontrará todas las bibliotecas cargadas al inicio del proceso, incluso las que se encuentran a través de
LD_LIBRARY_PATH
, pero no las cargadas posteriormente con dlopen()
.
Esto tiene una condición simple geteuid()>100
para minimizar problemas. No confía en RPATHS ni los maneja por separado de ninguna manera, por lo que este enfoque necesita algunos ajustes para dichos binarios. En su lugar, podría modificar de forma trivial el código abortado para iniciar sesión a través de syslog.
Si modifica /etc/ld.so.preload
y se equivoca, podría romper su sistema . (Tienes un shell de rescate enlazado estáticamente, ¿verdad?)
Podrías probar de forma controlada de manera controlada utilizando unshare
y mount --bind
para limitar su efecto (es decir, tener un /etc/ld.so.preload
privado). Necesitas root (o CAP_SYS_ADMIN
) para unshare
aunque:
#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;
}
Si sus usuarios acceden a través de ssh, entonces probablemente podrían usarse ForceCommand
y Match group
de OpenSSH, o un script de inicio personalizado para un "demonio sshd" de usuarios no confiables dedicado.
Para resumir: la única forma en que puede hacer exactamente lo que solicita (evitar LD_PRELOAD) es mediante el uso de un enlazador de tiempo de ejecución pirateado o más configurable. La anterior es una solución que le permite restringir las bibliotecas por una ruta de confianza, lo que elimina el aguijón de este tipo de malware sigiloso.
Como último recurso, podría obligar a los usuarios a usar sudo
para ejecutar todos los programas, esto limpiará muy bien su entorno y, como está configurado, no se verá afectado por sí mismo. Solo una idea ;-) Sobre el tema de sudo
, utiliza el mismo truco de biblioteca para evitar que los programas den los usuarios un shell de puerta trasera con su característica NOEXEC
.