preche?

In questo post, si è discusso di un tool che fa largo uso del preloading di librerie condivise; quindi, su richiesta del buon Roberto, cerco di dare una breve panoramica dell’argomento:

LD_PRELOAD (così come /etc/ld.so.preload, che però, essendo modificabile solo da root, non ha gli stessi limiti di LD_PRELOAD per i binari setuid/setgid) permette di istruire il loader a caricare delle librerie condivise, prima di tutte le altre (cioè prima di quelle nei path specificati in ld.so.conf, utilizzati da ldconfig per creare l’ld.so.cache che verrà usato dal loader).

L’aspetto interessante del poter forzare il caricamento di una o più librerie prima di tutte le altre, è la possibilità di forzare i programmi a far uso di un’altra implementazione di una data funzione, offerta dalle librerie standard; oppure il poter creare degli hook a funzioni delle suddette librerie standard.

Ma bando alle ciance e, come dice un famoso troll, “Talk is cheap. Show me the code”, quindi ho scritto una mini-libreria di esempio, che mostra come si possano fare degli hook a funzioni della libreria standard (per rimanere in tema di execv*() e setuid(), visto che eravamo partiti da questo nel post su valgrind :) )

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>

#ifndef RTLD_NEXT
#       define RTLD_NEXT        ((void *) -1l)
#endif

static int checksetuid(const char *filename)
{
        struct stat lalala;
        stat(filename,&lalala);
        if(lalala.st_mode & S_ISUID)
                return 1;
        return 0;
}

int execv(const char *filename, char *const argv[])
{
        int (*real_execv)(const char *filename, char *const argv[]) = dlsym(RTLD_NEXT,"execv");
        if(checksetuid(filename)) {
                errno = EACCES;
                return -1;
        }
        return (*real_execv)(filename,argv);
}

int execve(const char *filename, char *const argv[], char *const envp[])
{
        int (*real_execve)(const char *filename, char *const argv[], char *const envp[]) = dlsym(RTLD_NEXT,"execve");
        if(checksetuid(filename)) {
                errno = EACCES;
                return -1;
        }
        return (*real_execve)(filename,argv,envp);
}

mini:/tmp/exechook# gcc -shared -fomit-frame-pointer execve.c -oexecve.so -ldl
mini:/tmp/exechook# export LD_PRELOAD=/tmp/exechook/execve.so
mini:/tmp/exechook# bash
mini:/tmp/exechook# ls
execve.c execve.so
mini:/tmp/exechook# mount
bash: /bin/mount: Permission denied
mini:/tmp/exechook# exit
mini:/tmp/exechook# mount
/dev/md0 on / type ext3 (rw,noatime,nodiratime,user_xattr,errors=remount-ro)
tmpfs on /lib/init/rw type tmpfs (rw,nosuid,mode=0755)
proc on /proc type proc (rw,noexec,nosuid,nodev)
sysfs on /sys type sysfs (rw,noexec,nosuid,nodev)
udev on /dev type tmpfs (rw,mode=0755)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
devpts on /dev/pts type devpts (rw,noexec,nosuid,gid=5,mode=620)
fusectl on /sys/fs/fuse/connections type fusectl (rw)
/dev/mapper/md1_crypt on /home type ext3 (rw,noatime,nodiratime,user_xattr)
rpc_pipefs on /var/lib/nfs/rpc_pipefs type rpc_pipefs (rw)
nfsd on /proc/fs/nfsd type nfsd (rw)
mini:/tmp/exechook#

Quindi se il binario non è setuid (/bin/ls) tutto ok, altrimenti (/bin/mount è setuid) EACCES.

Il codice spero sia abbastanza chiaro, la libreria esporta le due funzioni execve() ed execv(), le suddette controllano che il file da eseguire non sia setuid; se lo è, tornano -1 e settano l’errore a EACCES, altrimenti chiamano la execv/execve della libc (usando un puntatore a funzione che si ricavano con la dlsym()) e tornano il valore tornato da quest’ultima.

I due hook possono funzionare solo grazie al fatto che la libreria viene caricata in preload, quindi quando un programma chiama le due funzioni, vengono utilizzate quelle della libreria in preload anzichè quelle della libc.

Il maggiore pregio di questa tecnica è il poter fare hook o sostituire qualunque funzione di qualunque libreria in maniera semplice (con la ptrace() esci pazzo) e rimanendo in userland (quindi codice portabile, niente ricompilazione del kernel con relativo reboot e niente sbattimenti a cercare la syscall_table nelle fratte in cui la spostano ogni volta che Torvalds si sveglia storto).

Il maggiore svantaggio è che basta utilizzare binari linkati staticamente per rendere completamente inutile la libreria in preload.

per approfondire (in ordine crescente di approfondimento :)

man ld.so; man dlsym; man ldconfig
http://tldp.org/HOWTO/Program-Library-HOWTO/index.html
http://sco.com/developers/gabi/latest/contents.html
http://www.trunix.org/programlama/os/cpu-elf/node1.html