Cannot use set_memory_rw in Linux kernel on ARM64
Asked Answered
R

1

1

I am trying to develop a kernel module that hooks the read() system call. for some reason the set_memory_rw() function does not seem to work.

I saw another question of this sort but I didn't really understand what to do.

I am working on Kali 4.19.93 with Raspberry-pi 4

My code:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/syscalls.h>
#include <linux/kallsyms.h>
#include <linux/slab.h>
#include <linux/kern_levels.h>
#include <asm/unistd.h>
#include <asm/cacheflush.h>
#include <linux/semaphore.h>
#include <asm/set_memory.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Omri Ben David");
MODULE_DESCRIPTION("Hooking Linux System calls");
MODULE_VERSION("1.0");

unsigned long** SYS_CALL_TABLE = (unsigned long**) 0xc02011c4;

asmlinkage ssize_t (*original_read) (int fd, char *buf, size_t count);
asmlinkage ssize_t HookRead(unsigned int fd, char * buf, size_t count)
{
    printk(KERN_INFO "Rootkit_Debug: Yay you entered my function!!\n Now you can read\n");
    return (*original_read)(fd,buf,count);
}   

void (*seek)(unsigned long, int);
void (*hide)(unsigned long, int);

static int __init SetHooks(void)
{
    printk(KERN_INFO "Hooks Will now be set, hold on tight\n");
    printk(KERN_INFO "System calls table is at address %p\n",SYS_CALL_TABLE);

    original_read = (void*) SYS_CALL_TABLE[__NR_read];
    
    seek = (void*) kallsyms_lookup_name("set_memory_rw");
    hide = (void*) kallsyms_lookup_name("set_memory_ro");

    (*seek)((unsigned long)SYS_CALL_TABLE, 1);
    SYS_CALL_TABLE[__NR_read] = (unsigned long*)HookRead;
    (*hide)((unsigned long)SYS_CALL_TABLE, 1);
    printk(KERN_INFO "System calls hooked successfully\n");

    return 0;
}

static void __exit HookCleanup(void)
{
    printk(KERN_INFO "System calls restore initiated\n");

    (*seek)((unsigned long)SYS_CALL_TABLE, 1);
    SYS_CALL_TABLE[__NR_read] = (unsigned long*) original_read;
    (*hide)((unsigned long)SYS_CALL_TABLE, 1);

    printk(KERN_INFO "System successfully restored. hope you had fun");
}

module_init(SetHooks);
module_exit(HookCleanup);

How can I make the set_memory_rw() function work in order to overwrite the syscall table? Or should I use another method?

Ruminate answered 16/4, 2020 at 10:18 Comment(8)
Have you tried to #include <linux/set_memory.h>?Mccowan
The set_memory_rw() function is not exported, so you cannot use it directly in your module. Trying to use it in your module will cause the compilation to fail. You could get a pointer to it at runtime using kallsyms_lookup_name("set_memory_rw").Ophiolatry
@MarcoBonelli I did that and it compiles successfully. However it seems that accessing this function causes a runtime error: "Unable to handle paging request at virtual address [function address]Ruminate
I also edited the code in my question to the new code formRuminate
That's because you cannot use those functions on that memory area: elixir.bootlin.com/linux/v4.19.13/source/arch/arm64/mm/…Ophiolatry
@MarcoBonelli Then how would suggest doing that?Ruminate
@obd I was thinking about it... I've got an Idea, but I'm not sure it's 100% right. I don't know that much about ARM. Ideally all you need would be to find the PTE for the page in which the syscall table resides and update the pgprot bits to make it RW, then flush the TLB to make sure changes are reflected. In practice that comment in the above link confuses me. I'll test it out and answer when I can, maybe tomorrow since I've got a busy day ahead.Ophiolatry
@MarcoBonelli Thank you so much for helping! it really means a lotRuminate
O
2

Note: for an updated version of the code in this answer that also covers the case where huge mappings are used for sys_call_table (not currently handled by the code here, which fails with -EINVAL), see this other answer of mine.


So, as I said in the comments above, it appears that the function change_memory_common() (which is used by set_memory_ro/rw()) does a check before applying the requested permissions. This is documented with a comment:

/*
 * Kernel VA mappings are always live, and splitting live section
 * mappings into page mappings may cause TLB conflicts. This means
 * we have to ensure that changing the permission bits of the range
 * we are operating on does not result in such splitting.
 *
 * Let's restrict ourselves to mappings created by vmalloc (or vmap).
 * Those are guaranteed to consist entirely of page mappings, and
 * splitting is never needed.
 *
 * So check whether the [addr, addr + size) interval is entirely
 * covered by precisely one VM area that has the VM_ALLOC flag set.
 */
area = find_vm_area((void *)addr);
if (!area ||
    end > (unsigned long)area->addr + area->size ||
    !(area->flags & VM_ALLOC))
    return -EINVAL;

The function seems to only work for mappings created through vmalloc() or vmap(), and the sys_call_table does not reside in a mapping of such kind.

The concern seems to be around TLB conflicts. It may be that this function does not split huge pages and therefore cannot set permissions with single (non-huge) page granularity. You will need to test this, it will either work or not work for your specific case.

In any case, for the purpose of your exercise on syscall hijacking, you can re-write your own version of set_memory_common() and set_memory_rw/ro() avoiding this check. An easier way would be to just get the appropriate PTE for the desired address and then change the permissions, but I didn't look through all the countless macros for that.

Last, but not least, since the sys_call_table could end up crossing a page boundary, it's better to use syscall_table + __NR_read instead of just sys_call_table when applying changes to the page.

Here's a working example:

// SPDX-License-Identifier: GPL-3.0
#include <linux/init.h>     // module_{init,exit}()
#include <linux/module.h>   // THIS_MODULE, MODULE_VERSION, ...
#include <linux/kernel.h>   // printk(), pr_*()
#include <linux/kallsyms.h> // kallsyms_lookup_name()
#include <asm/syscall.h>    // syscall_fn_t, __NR_*
#include <asm/ptrace.h>     // struct pt_regs
#include <asm/tlbflush.h>   // flush_tlb_kernel_range()
#include <asm/pgtable.h>    // {clear,set}_pte_bit(), set_pte()
#include <linux/vmalloc.h>  // vm_unmap_aliases()
#include <linux/mm.h>       // struct mm_struct, apply_to_page_range()
#include <linux/kconfig.h>  // IS_ENABLED()

#ifdef pr_fmt
#undef pr_fmt
#endif
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

static struct mm_struct *init_mm_ptr;
static syscall_fn_t *syscall_table;
static syscall_fn_t original_read;

/********** HELPERS **********/

// From arch/arm64/mm/pageattr.c.
struct page_change_data {
    pgprot_t set_mask;
    pgprot_t clear_mask;
};

// From arch/arm64/mm/pageattr.c.
static int change_page_range(pte_t *ptep, unsigned long addr, void *data)
{
    struct page_change_data *cdata = data;
    pte_t pte = READ_ONCE(*ptep);

    pte = clear_pte_bit(pte, cdata->clear_mask);
    pte = set_pte_bit(pte, cdata->set_mask);

    set_pte(ptep, pte);
    return 0;
}

// From arch/arm64/mm/pageattr.c.
static int __change_memory_common(unsigned long start, unsigned long size,
                  pgprot_t set_mask, pgprot_t clear_mask)
{
    struct page_change_data data;
    int ret;

    data.set_mask = set_mask;
    data.clear_mask = clear_mask;

    ret = apply_to_page_range(init_mm_ptr, start, size, change_page_range, &data);

    flush_tlb_kernel_range(start, start + size);
    return ret;
}

// Simplified set_memory_rw() from arch/arm64/mm/pageattr.c.
static int set_page_rw(unsigned long addr)
{
    vm_unmap_aliases();    
    return __change_memory_common(addr, PAGE_SIZE, __pgprot(PTE_WRITE), __pgprot(PTE_RDONLY));
}

// Simplified set_memory_ro() from arch/arm64/mm/pageattr.c.
static int set_page_ro(unsigned long addr)
{
    vm_unmap_aliases();
    return __change_memory_common(addr, PAGE_SIZE, __pgprot(PTE_RDONLY), __pgprot(PTE_WRITE));
}

/********** ACTUAL MODULE **********/

static long myread(const struct pt_regs *regs)
{
    pr_info("read() called\n");
    return original_read(regs);
}

static int __init modinit(void)
{
    int res;

    pr_info("init\n");

    // Shouldn't fail.
    init_mm_ptr = (struct mm_struct *)kallsyms_lookup_name("init_mm");
    syscall_table = (syscall_fn_t *)kallsyms_lookup_name("sys_call_table");

    original_read = syscall_table[__NR_read];

    res = set_page_rw((unsigned long)(syscall_table + __NR_read) & PAGE_MASK);
    if (res != 0) {
        pr_err("set_page_rw() failed: %d\n", res);
        return res;
    }

    syscall_table[__NR_read] = myread;

    res = set_page_ro((unsigned long)(syscall_table + __NR_read) & PAGE_MASK);
    if (res != 0) {
        pr_err("set_page_ro() failed: %d\n", res);
        return res;
    }

    pr_info("init done\n");

    return 0;
}

static void __exit modexit(void)
{
    int res;

    pr_info("exit\n");

    res = set_page_rw((unsigned long)(syscall_table + __NR_read) & PAGE_MASK);
    if (res != 0) {
        pr_err("set_page_rw() failed: %d\n", res);
        return;
    }

    syscall_table[__NR_read] = original_read;

    res = set_page_ro((unsigned long)(syscall_table + __NR_read) & PAGE_MASK);
    if (res != 0)
        pr_err("set_page_ro() failed: %d\n", res);

    pr_info("goodbye\n");
}

module_init(modinit);
module_exit(modexit);
MODULE_VERSION("0.1");
MODULE_DESCRIPTION("Syscall hijack on arm64.");
MODULE_AUTHOR("Marco Bonelli");
MODULE_LICENSE("GPL");
Ophiolatry answered 27/4, 2020 at 18:29 Comment(8)
Hi, Marco, I tried your code and it does not work for me: I need to patch a couple of bytes in the kernel code area... anyway I tried your module AS IT IS and my phone reboots. Android 12 kernel 4.14.190.Bifoliate
@Bifoliate well that's interesting. I did not try patching kernel text, nor did I try my code on an Android kernel, I only tested it on Linux arm64 built with defconfig and running on QEMU. Maybe you can ask a question about it showing your code and setup, describing the issue in more detail.Ophiolatry
Marco, I added you on facebook if you want to further discuss it...Bifoliate
@Bifoliate I do not usually provide help outside StackOverflow, unless it's a special case or a simple matter, which this one does not seem to be. As I said, you should ask a new question.Ophiolatry
well... you answer does not work on ARM64 android. I tested it and the system crashes.Bifoliate
@Bifoliate I got that, sorry but I don't know what to tell you. I don't have an Android kernel build or device to test on, so you'll have to figure it out by yourself (or ask another question as I said).Ophiolatry
I am guessing you don't have an android phone in 2022 then... ok.. nevermind.Bifoliate
@Bifoliate yeah unfortunately I do not.Ophiolatry

© 2022 - 2024 — McMap. All rights reserved.