¿Hay alguna forma de bloquear LD_PRELOAD y LD_LIBRARY_PATH en Linux?

6

¿Preferiblemente con un kernel de vainilla Linux, es decir, que no use SELinux o GrSecurity?

Últimamente ha habido un caso de malware de servidor UNIX in-the-wild:

enlace

que aprovecha LD_PRELOAD para implementar un troyano de espacio de usuario oculto. Creo que sería bueno evitar que los usuarios limitados jueguen con las rutas de la biblioteca compartida, si es posible.

(Iría más lejos, por ejemplo, GrSecurity / confianza en la ejecución de la ruta, pero eso no es práctico en este momento).

¿Hay alguna forma en que pueda hacer esto de manera confiable?

Editar: lo que quise decir anteriormente es que, normalmente, cualquier usuario puede usar LD_PRELOAD para inyectar cualquier biblioteca en cualquier ejecutable que pueda ejecutar, y eso no es setuid ni setgid. Este es un mecanismo conocido para crear malware oculto y persistente. ¿Hay alguna manera de evitar que un usuario determinado utilice LD_PRELOAD en absoluto ?

    
pregunta DanL4096 21.07.2014 - 18:30
fuente

3 respuestas

9

Básicamente, es necesario controlar el entorno de ejecución de las aplicaciones. No hay magia al respecto. Un par de soluciones que vienen a la mente:

  1. De alguna manera, podrías establecer todos los binarios que te preocupan como setuid / setgid (eso no significa que deban ser propiedad de root, que yo sepa). Normalmente, Linux evita la conexión a un proceso setuid / setgid . ¡Por favor, verifique si lo hace para setuid que no es de raíz!

  2. Puede usar un cargador seguro para ejecutar sus aplicaciones en lugar de ld, que se niega a reconocer LD_PRELOADs. Esto puede romper algunas aplicaciones existentes. Consulte trabajo de Mathias Payer para obtener más información, aunque dudo que exista alguna herramienta disponible que pueda aplicar.

  3. Puedes reconstruir tus binarios con una libc que deshabilita LD_PRELOAD y dlsym. He escuchado que musl puede hacer eso si pasa las opciones correctas, pero no puede encontrar información sobre cómo hacerlo en este momento.

  4. Y, por último, podría aislar sus aplicaciones y evitar que las aplicaciones inicien directamente otros procesos con un entorno personalizado o que modifiquen el directorio de inicio del usuario. Tampoco hay una herramienta preparada para esto (hay mucho trabajo en progreso y aún no se puede desplegar nada).

Es probable que existan límites para las soluciones anteriores y otras soluciones candidatas según las aplicaciones que necesite ejecutar, quiénes son los usuarios y cuál es el modelo de amenaza. Si puede hacer que su pregunta sea más precisa, intentaré mejorar esa respuesta en consecuencia.

Editar: tenga en cuenta que un usuario malintencionado solo puede modificar su propio entorno de ejecución (a menos que pueda aumentar los privilegios de root con algún exploit, pero luego tiene que resolver otros problemas). Por lo tanto, un usuario generalmente no usaría inyecciones LD_PRELOAD porque ya pueden ejecutar código con los mismos privilegios. Los ataques tienen sentido para algunos escenarios:

  • interrumpir las verificaciones relacionadas con la seguridad en el lado del cliente del software cliente-servidor (generalmente hace trampas en los videojuegos o hacer que una aplicación cliente omita algún paso de validación con el servidor de su distribuidor)
  • instalando malware permanente cuando tomas el control de la sesión o el proceso de un usuario (ya sea porque se olvidaron de cerrar sesión y tienes acceso físico al dispositivo o porque explotaste una de sus aplicaciones con contenido diseñado)
respondido por el Steve DL 21.07.2014 - 18:59
fuente
6

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 .

    
respondido por el mr.spuratic 23.07.2014 - 00:47
fuente
1

Sí, hay una manera: no permita que ese usuario ejecute código arbitrario. Dales un shell restringido, o mejor, solo un conjunto predefinido de comandos.

No evitaría que se ejecute ningún malware, a menos que haya usado algún mecanismo de escalamiento de privilegios no estándar que no borre estas variables. Los mecanismos normales de escalamiento de privilegios (ejecutables setuid, setgid o setcap; llamadas entre procesos) ignoran estas variables. Entonces, no se trata de prevenir el malware, solo se trata de detectar malware.

LD_PRELOAD y LD_LIBRARY_PATH le permite a un usuario ejecutar ejecutables instalados y hacer que se comporten de manera diferente. Gran cosa: el usuario puede ejecutar sus propios ejecutables (incluidos los enlazados estáticamente). Todo lo que obtendrías es un poco de responsabilidad si estás registrando todas las llamadas execve . Pero si confías en eso para detectar malware, hay tanto que puede escapar de tu vigilancia que no me molestaría. Muchos lenguajes de programación ofrecen recursos similares a LD_LIBRARY_PATH : CLASSPATH , PERLLIB , PYTHONPATH , etc. No los incluirá en la lista negra de todos, solo sería útil un enfoque de lista blanca.

Como mínimo, también deberías bloquear ptrace : con ptrace , se puede hacer cualquier ejecutable para ejecutar cualquier código. El bloqueo de ptrace puede ser una buena idea, pero principalmente porque se han encontrado tantas vulnerabilidades a su alrededor que es probable que algunas queden sin descubrir.

Con un shell restringido, las variables LD_* son realmente una preocupación, porque el usuario solo puede ejecutar un conjunto de programas preaprobados y LD_* les permite evitar esta restricción. Algunos shells restringidos permiten que las variables sean de solo lectura.

    
respondido por el Gilles 22.07.2014 - 17:15
fuente

Lea otras preguntas en las etiquetas