xcellerator / linux_kernel_hacking

Linux Kernel Hacking
GNU General Public License v2.0
640 stars 126 forks source link

kallsyms_lookup_name is not exported anymore in kernels > 5.7 #3

Closed f0lg0 closed 3 years ago

f0lg0 commented 3 years ago

Issue

In newer kernel versions (> 5.7.0) the function kallsyms_lookup_name, used in your ftrace_helper.h library, is not exported anymore by default. This means that compiling the code provided by you (also found here) on newer kernels will fail throwing: ERROR: modpost: "kallsyms_lookup_name" undefined!

More references:

https://lkml.org/lkml/2020/2/25/576 https://lwn.net/Articles/813350/

Solution

I've done some research online and found this workaround, which compiles and works on Manjaro with kernel 5.9.16; so I have decided to link this awesome solution to your library.

patch.c - here's the solution wrote by @zizzu0 and modified by me

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>

// added by me
#include "inc/patch.h"

#define KPROBE_PRE_HANDLER(fname) static int __kprobes fname(struct kprobe *p, struct pt_regs *regs)

long unsigned int kln_addr = 0;
unsigned long (*kln_pointer)(const char* name) = NULL;

static struct kprobe kp0, kp1;

KPROBE_PRE_HANDLER(handler_pre0) {
    kln_addr = (--regs->ip);

    return 0;
}

KPROBE_PRE_HANDLER(handler_pre1) {
    return 0;
}

static int do_register_kprobe(struct kprobe* kp, char* symbol_name, void* handler) {
    int ret;

    kp->symbol_name = symbol_name;
    kp->pre_handler = handler;

    ret = register_kprobe(kp);
    if (ret < 0) {
        pr_err("do_register_kprobe: failed to register for symbol %s, returning %d\n", symbol_name, ret);
        return ret;
    }

    pr_info("Planted krpobe for symbol %s at %p\n", symbol_name, kp->addr);

    return ret;
}

// this is the function that I have modified, as the name suggests it returns a pointer to the extracted kallsyms_lookup_name function
kln_p get_kln_p(void) {
    int status;

    status = do_register_kprobe(&kp0, "kallsyms_lookup_name", handler_pre0);

    if (status < 0) return NULL;

    status = do_register_kprobe(&kp1, "kallsyms_lookup_name", handler_pre1);

    if (status < 0) {
        // cleaning initial krpobe
        unregister_kprobe(&kp0);
        return NULL;
    }

    unregister_kprobe(&kp0);
    unregister_kprobe(&kp1);

    pr_info("kallsyms_lookup_name address = 0x%lx\n", kln_addr);

    kln_pointer = (unsigned long (*)(const char* name)) kln_addr;

    return kln_pointer;
}

usable by including patch.h

#ifndef PATCH_H
#define PATCH_H

typedef unsigned long (*kln_p)(const char*);
kln_p get_kln_p(void);

#endif

And finally, the patched function in the lib ftracer_hekper.h

#include "patch.h"

static int fh_resolve_hook_address(struct ftrace_hook* hook) {
    // new method
    kln_p kln = get_kln_p();
    if (kln == NULL) return -1;

    hook->address = kln(hook->name);

    if (!hook->address) {
        printk(KERN_DEBUG "rootkit: 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;
}

Conclusion

The described methods works also on older kernels, like in the 5.4.0-58 used in the Vagrant instance provided by your blog. It works by extracting the dynamic address from the kernel. I haven't opened a pull request because I couldn't find the ftracer_helper lib here (I have only found it as a gist) and especially because I am a begginner in kernel land, my code could have been written and integrated better. It was a cool exercise for a beginner like me, hope you will find it useful!

Sorry for my English, I ain't a native speaker :)

xcellerator commented 3 years ago

Hi! Thanks for bringing this up - I've been thinking about what the best way to get around this problem would be for a while now.

The lack of kallsyms_lookup_name() is definitely annoying, and your method is pretty cool. The way I've been tackling it so far has been to work out the kernel's load address from a function in low memory that is exported (I'm using sprint_symbol() at the moment, but there's probably something better), and then brute force upwards, using sprint_symbol() to resolve the name of any function at each address. Luckily, syscalls and interesting functions (like procfile read/write handlers) are pretty early in memory, so the delay for brute-forcing is hardly noticeable.

The function I'm using (e.g. here in a new branch) looks like this:

unsigned long kaddr_lookup_name(const char *fname_raw)
{
    int i;
    unsigned long kaddr;
    char *fname_lookup, *fname;

    fname_lookup = kzalloc(NAME_MAX, GFP_KERNEL);
    if (!fname_lookup)
        return 0;

    fname = kzalloc(strlen(fname_raw)+4, GFP_KERNEL);
    if (!fname)
        return 0;

    /*
     * We have to add "+0x0" to the end of our function name
     * because that's the format that sprint_symbol() returns
     * to us. If we don't do this, then our search can stop
     * prematurely and give us the wrong function address!
     */
    strcpy(fname, fname_raw);
    strcat(fname, "+0x0");

    /*
     * Get the kernel base address:
     * sprint_symbol() is less than 0x100000 from the start of the kernel, so
     * we can just AND-out the last 3 bytes from it's address to the the base
     * address.
     * There might be a better symbol-name to use?
     */
    kaddr = (unsigned long) &sprint_symbol;
    kaddr &= 0xffffffffff000000;

    /*
     * All the syscalls (and all interesting kernel functions I've seen so far)
     * are within the first 0x100000 bytes of the base address. However, the kernel
     * functions are all aligned so that the final nibble is 0x0, so we only
     * have to check every 16th address.
     */
    for ( i = 0x0 ; i < 0x100000 ; i++ )
    {
        /*
         * Lookup the name ascribed to the current kernel address
         */
        sprint_symbol(fname_lookup, kaddr);

        /*
         * Compare the looked-up name to the one we want
         */
        if ( strncmp(fname_lookup, fname, strlen(fname)) == 0 )
        {
            /*
             * Clean up and return the found address
             */
            kfree(fname_lookup);
            return kaddr;
        }
        /*
         * Jump 16 addresses to next possible address
         */
        kaddr += 0x10;
    }
    /*
     * We didn't find the name, so clean up and return 0
     */
    kfree(fname_lookup);
    return 0;
}

I've also pushed my working branch to GitHub here (in my testing so far, all the modules that use ftrace_helper.h work again on kernel 5.10.5-arch1-1).

I'd be interested to hear what you think!

f0lg0 commented 3 years ago

Hi! That solution is pretty neat but I am worried about performance (and about the percentage of failure), even though you have underlined the fact that it's barely noticeable. I am really impressed by your solution though, I am still not pretty good at bytes operations so I would have never thought of that. I was looking around trying to find another solution and these are the ideas that came to my mind:

static struct kprobe kp = { .symbol_name = "kallsyms_lookup_name" };

int __init init_test(void) { register_kprobe(&kp);

// x --> unsigned hexadecimal integer
pr_alert("Found at 0x%px \n", kp.addr);

return 0;

}

void __exit cleanup_test(void) { unregister_kprobe(&kp); }

MODULE_LICENSE("GPL");

module_init(init_test); module_exit(cleanup_test);


Output in the kernel logs:
`f0lg0-pc kernel: Found at 0xffffffff9c13c480`

Output in `/proc/kallsysm`:

ffffffff9c13bc50 T module_kallsyms_lookup_name ffffffff9c13c480 T kallsyms_lookup_name



I haven't tried to hook it up to the lib but the addresses are equal.

- The last method was described in this [Tweet](https://twitter.com/spendergrsec/status/1298336464632262659), I haven't fully looked into it yet but it seems kinda cool and it's kinda similar to yours. It uses `sprint_symbol_no_offset() in a loop over the kernel image for just one trivial example` to find the address of `loglevel`.

Thank you for replying to me and let me know what you think, looking forward on finding a stable solution! 
xcellerator commented 3 years ago

Now this is a lovely solution! I threw it into this branch (just for the root backdoor for now) and it works really nicely!

Early on, I check the kernel version, and setup the struct:

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,7,0)
#define KPROBE_LOOKUP 1
#include <linux/kprobes.h>
static struct kprobe kp = {
    .symbol_name = "kallsyms_lookup_name"
};
#endif

and then later on, resolve the kallsyms_lookup_name() symbol in fh_resolve_hook_address():

#ifdef KPROBE_LOOKUP
    typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
    kallsyms_lookup_name_t kallsyms_lookup_name;
    register_kprobe(&kp);
    kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr;
    unregister_kprobe(&kp);
#endif

I'm gonna test the other techniques to make sure that they all work properly, but this definitely looks like the one to go with.

f0lg0 commented 3 years ago

Awesome! Completely agree, it seems the shortest and quickest way to do it. I don't know enough about kprobes performance but it looks reasonable. Plus all the sweet checking you do to trigger this patch only if needed makes it perfect. Glad I helped you and thanks for your awesome work!

xcellerator commented 3 years ago

These changes have been merged into master (and I credited you with the idea in a comment!). Thanks again for the sweet find!

ncorbuk commented 3 years ago

Excellent!

BrunoCiccarino commented 3 years ago

The best solution I found on the internet, thanks. ;-p

NicholasBHubbard commented 11 months ago

This kprobe method depends on CONFIG_KPROBE and CONFIG_KALLSYMS to be set. At least Slackware 15.0 does not have CONFIG_KPROBE set, and I suspect there are others as well.

Relevant docs: https://docs.kernel.org/trace/kprobes.html#configuring-kprobes

OpaxIV commented 1 month ago

Just for my understanding: why is the usage of both kallsyms_lookup_name AND kprobes needed? I may got something twisted because in my head both try to reach the same goal right? finding the address of the hooked syscall? thx for the replies