Esta nota fue escrita en 2014, pero justo caí bajo represión en Habr y no salió a la luz. Durante el tiempo que estuve baneado, me olvidé de ella, y ahora la encontré en los borradores. Pensé en borrarla, pero quizás a alguien le sirva.

En resumen, una pequeña lectura de viernes para administradores sobre la búsqueda de lo "activado" LD_PRELOAD.
1. Un pequeño desvío para aquellos que no están familiarizados con la suspensión de funciones
Los demás pueden ir directamente a p.2.
Empezaremos con un 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 ("%dn", rand()%100);
}
}
Compilamos sin ningún flag:
$ gcc ./ld_rand.c -o ld_rand
Y, como era de esperar, obtenemos 5 números aleatorios menores de 100:
$ ./ld_rand
53
93
48
57
20
Pero supongamos que no tenemos el código fuente del programa y necesitamos cambiar su comportamiento.
Crearemos nuestra 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 completamente predecible:
# LD_PRELOAD=$PWD/ld_rand.so ./ld_rand
42
42
42
42
42
Este truco parece aún más impresionante si primero exportamos nuestra biblioteca a través de
$ export LD_PRELOAD=$PWD/ld_rand.so
o ejecutamos previamente
# echo "$PWD/ld_rand.so" > /etc/ld.so.preload
y luego ejecutamos el programa de forma habitual. No hemos cambiado ni una línea del código del programa en sí, pero su comportamiento ahora depende de una pequeña función en nuestra biblioteca. Además, en el momento de escribir este programa, el falso rand ni siquiera existía.
¿Qué llevó a nuestro programa a usar el falso? rand? Разберем по шагам.
Cuando se inicia la aplicación, se cargan ciertas bibliotecas que contienen funciones necesarias para el programa. Podemos verlas 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 incluir obligatoriamente el archivo libc.so. Esta biblioteca proporciona llamadas al sistema y funciones básicas como open, malloc, printf y demás. Nuestro rand también está entre ellas. Verifiquemos esto:
# nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " rand$"
000000000003aef0 T rand
Veamos si el conjunto de bibliotecas cambia 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 hace que se cargue nuestra ld_rand.so incluso cuando el programa no la solicita. Y, dado que nuestra función "rand" se carga antes que rand desde libc.so, ella toma el control.
Está bien, logramos reemplazar la función original, pero ¿cómo hacer para que su funcionalidad se mantenga y se agreguen ciertas acciones? Modificamos nuestro aleatorio:
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
typedef int (*orig_rand_f_type)(void);
int rand()
{
/* Выполняем некий код */
printf("Evil injected coden");
orig_rand_f_type orig_rand;
orig_rand = (orig_rand_f_type)dlsym(RTLD_NEXT,"rand");
return orig_rand();
}
Aquí, como nuestra "adición", solo imprimimos una línea de texto, después de lo cual creamos un puntero a la función original. rand. Para obtener la dirección de esta función, necesitaremos dlsym — es una función de la biblioteca libdl, que encontrará nuestro rand en la pila de bibliotecas dinámicas. Después de eso, llamaremos a esta función y devolveremos su valor. Por lo tanto, necesitaremos añadir «-ldl» al compilar:
$ gcc -ldl -shared -fPIC .\/o_rand_evil.c -o ld_rand_evil.so
$ LD_PRELOAD=$PWD\/ld_rand_evil.so .\/ld_rand
Código inyectado maligno
66
Código inyectado maligno
28
Código inyectado maligno
93
Código inyectado maligno
93
Código inyectado maligno
95
Y nuestro programa utiliza «nativo» rand, realizando previamente algunas acciones cuestionables.
2. La tortura de la búsqueda
Sabiendo de la amenaza potencial, queremos detectar lo que preload se ha ejecutado. Está claro que la mejor manera de detectarlo es integrarlo en el núcleo, pero yo estaba interesado en opciones de detección en espacio de usuario.
A continuación, vendrán soluciones para la detección y su refutación por pares.
2.1. Comencemos con lo simple
Como se mencionó anteriormente, se puede especificar la biblioteca que se carga mediante la variable LD_PRELOAD o especificándola en el archivo /etc/ld.so.preload. Crearemos dos detectores muy básicos.
El primero — para 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 — para verificar la apertura de archivos:
#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");
}
Carguemos las 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) [+]
Aquí y en adelante, [+] indica una detección exitosa.
Por lo tanto, [-] significa que se evitó la detección.
¿Qué tan efectivo es un detector así? Primero abordemos 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) [-]
De manera similar, eliminamos la verificación open:
#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 utilizar otros métodos de acceso a archivos, como, open64, stat etc., pero, en esencia, para engañarlos se necesitan las mismas 5-10 líneas de código.
2.2. Avancemos
Antes usamos getenv() para obtener el valor LD_PRELOAD, pero hay una forma más «bajo nivel» de llegar a ENV-variables. No utilizaremos funciones intermedias, sino que accederemos al arreglo **environ, en el 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;
}
Dado que aquí leemos los datos directamente de la memoria, esta llamada no se puede interceptar, y nuestra undetect_getenv ya no obstaculiza la determinación de la intrusión.
$ LD_PRELOAD=.\/ld_undetect_getenv.so .\/detect_environ
LD_PRELOAD (**environ) [+]
¿Parece que con esto el problema está resuelto? Apenas está comenzando.
Una vez que el programa está en ejecución, el valor de la variable LD_PRELOAD en la memoria ya no es necesario para los atacantes, es decir, se puede considerar y eliminar antes de ejecutar cualquier instrucción. Por supuesto, modificar un arreglo en memoria es, como mínimo, un mal estilo de programación, pero ¿puede eso detener a alguien que ya no desea nuestro bien?
Para esto, necesitamos crear nuestra propia función falsa init(), en la que interceptaremos la establecida LD_PRELOAD y se la pasaremos 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;
}
Ejecutamos, comprobamos:
$ 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 se puede detectar una sustitución LD_PRELOAD, también existe /proc/. Comencemos con lo obvio /proc/{PID}/environ.
De hecho, hay una solución universal para la undetección **environ y /proc/self/environ. El problema radica en el comportamiento "incorrecto" de unsetenv(env).
la versión correcta
void evil_init()
{
\/\/ Primero guardamos el valor actual de 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) {
\/\/ Deshacemos el actual LD_PRELOAD
\/\/unsetenv(env);
\/\/ En lugar de unset simplemente nullificamos nuestra variable
for(int j = 0; environ[i][j] != ' '; j++) environ[i][j] = ' ';
break;
}
}
}
$ 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 " 00" "n" | grep -F LD_PRELOAD
$
Pero imaginemos que no lo encontramos y /proc/self/environ contiene datos "problemáticos".
Primero intentemos con nuestra anterior "disfraz":
$ (LD_PRELOAD=.\/ld_undetect_environ.so cat \/proc\/self\/environ; echo) | tr " 00" "n" | grep -F LD_PRELOAD
LD_PRELOAD=.\/ld_undetect_environ.so
cat usa para abrir el archivo el mismo open(), por lo que la solución es similar a lo que ya se hizo en 2.1, pero ahora creamos un archivo temporal, donde copiamos los valores de la memoria real excepto las líneas que contienen 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 != ' '; ptr++) {
found = 1;
for(i = 0; found == 1 && sub[i] != ' '; 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 == ' ') {
//printf("n");
if(!sstrstr(buffer, "LD_PRELOAD")) {
if((wbytes = write(fh,buffer,k))==-1) {
//printf("write errorn");
}
else {
//printf("writed %dn", 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 este paso ha sido completado:
$ (LD_PRELOAD=.\/ld_undetect_proc_environ.so cat \/proc\/self\/environ; echo) | tr " 00" "n" | grep -F LD_PRELOAD
$
El siguiente lugar obvio es /proc/self/maps. No vale la pena quedarse en él. La solución es exactamente la misma que la anterior: copiamos los datos del archivo excluyendo las líneas entre libc.so y ld.so.
2.4. Variante de Chokepoint
Esta solución me gustó especialmente por su simplicidad. Comparamos las direcciones de las funciones cargadas directamente desde libc, y las direcciones de "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: %pn", libc_func);
printf("Next address: %pn", next_func);
}
else {
printf("LD_PRELOAD (syscall - %s) [-]n", syscall_open);
}
return 0;
}
Cargamos la biblioteca con la intercepción de "open()" y comprobamos:
$ export LD_PRELOAD=$PWD/ld_undetect_open.so
$ ./detect_chokepoint
LD_PRELOAD (syscall - open) [+]
Dirección de Libc: 0x7fa86893b160
Próxima dirección: 0x7fa868a26135
La refutación resultó ser aún más sencilla:
#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. Syscalls
En teoría esto sería todo, pero aún podemos profundizar. Si dirigimos la llamada al sistema directamente al núcleo, esto permitirá eludir todo el proceso de intercepción. La solución a continuación, por supuesto, es dependiente de la arquitectura (x86_64). Intentemos implementarlo para la detección de apertura ld.so.preload.
#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 esta tarea tiene solución. Un extracto de man‘a:
ptrace es una herramienta que permite a un proceso padre observar y controlar la ejecución de otro proceso, así como ver y modificar sus datos y registros. Normalmente, esta función se utiliza para establecer puntos de interrupción en programas de depuración y rastrear llamadas al sistema.
El proceso padre puede comenzar la trazabilidad llamando primero a la función fork(2), y luego el proceso hijo resultante puede ejecutar PTRACE_TRACEME, seguido (por lo general) de la ejecución de exec(3). Por otro lado, el proceso padre puede comenzar a depurar un proceso existente usando PTRACE_ATTACH.
Durante la trazabilidad, el proceso hijo se detiene cada vez que recibe una señal, incluso si esa señal es ignorada. (Una excepción es SIGKILL, que funciona normalmente.) El proceso padre será notificado sobre esto al llamar wait(2), tras lo cual puede examinar y modificar el contenido del proceso hijo antes de su reanudación. Después, el proceso padre permite que el hijo continúe, ignorando en algunos casos la señal enviada o enviando en su lugar otra señal).
Por lo tanto, la solución consiste en rastrear el proceso, deteniéndolo antes de cada llamada al sistema y, si es necesario, redirigiendo el flujo a una función de trampa.
#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);
}
}
Verificando:
$ ./detect_syscall
LD_PRELOAD (open syscall) [+]
$ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall
LD_PRELOAD (open syscall) [-]
+0-0=5
Muchas gracias
, cuyos artículos, fuentes y comentarios contribuyeron mucho más que yo a que esta nota apareciera aquí.
Fuente: habr.com
