Linux Kernel: System call hooking example

Viewed 77260

I'm trying to write some simple test code as a demonstration of hooking the system call table.

"sys_call_table" is no longer exported in 2.6, so I'm just grabbing the address from the System.map file, and I can see it is correct (Looking through the memory at the address I found, I can see the pointers to the system calls).

However, when I try to modify this table, the kernel gives an "Oops" with "unable to handle kernel paging request at virtual address c061e4f4" and the machine reboots.

This is CentOS 5.4 running 2.6.18-164.10.1.el5. Is there some sort of protection or do I just have a bug? I know it comes with SELinux, and I've tried putting it in to permissive mode, but it doesn't make a difference

Here's my code:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/unistd.h>

void **sys_call_table;

asmlinkage int (*original_call) (const char*, int, int);

asmlinkage int our_sys_open(const char* file, int flags, int mode)
{
   printk("A file was opened\n");
   return original_call(file, flags, mode);
}

int init_module()
{
    // sys_call_table address in System.map
    sys_call_table = (void*)0xc061e4e0;
    original_call = sys_call_table[__NR_open];

    // Hook: Crashes here
    sys_call_table[__NR_open] = our_sys_open;
}

void cleanup_module()
{
   // Restore the original call
   sys_call_table[__NR_open] = original_call;
}
5 Answers

I finally found the answer myself.

http://www.linuxforums.org/forum/linux-kernel/133982-cannot-modify-sys_call_table.html

The kernel was changed at some point so that the system call table is read only.

cypherpunk:

Even if it is late but the Solution may interest others too: In the entry.S file you will find: Code:

.section .rodata,"a"
#include "syscall_table_32.S"

sys_call_table -> ReadOnly You have to compile the Kernel new if you want to "hack" around with sys_call_table...

The link also has an example of changing the memory to be writable.

nasekomoe:

Hi everybody. Thanks for replies. I solved the problem long ago by modifying access to memory pages. I have implemented two functions that do it for my upper level code:

#include <asm/cacheflush.h>
#ifdef KERN_2_6_24
#include <asm/semaphore.h>
int set_page_rw(long unsigned int _addr)
{
    struct page *pg;
    pgprot_t prot;
    pg = virt_to_page(_addr);
    prot.pgprot = VM_READ | VM_WRITE;
    return change_page_attr(pg, 1, prot);
}

int set_page_ro(long unsigned int _addr)
{
    struct page *pg;
    pgprot_t prot;
    pg = virt_to_page(_addr);
    prot.pgprot = VM_READ;
    return change_page_attr(pg, 1, prot);
}

#else
#include <linux/semaphore.h>
int set_page_rw(long unsigned int _addr)
{
    return set_memory_rw(_addr, 1);
}

int set_page_ro(long unsigned int _addr)
{
    return set_memory_ro(_addr, 1);
}

#endif // KERN_2_6_24

Here's a modified version of the original code that works for me.

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/unistd.h>
#include <asm/semaphore.h>
#include <asm/cacheflush.h>

void **sys_call_table;

asmlinkage int (*original_call) (const char*, int, int);

asmlinkage int our_sys_open(const char* file, int flags, int mode)
{
   printk("A file was opened\n");
   return original_call(file, flags, mode);
}

int set_page_rw(long unsigned int _addr)
{
   struct page *pg;
   pgprot_t prot;
   pg = virt_to_page(_addr);
   prot.pgprot = VM_READ | VM_WRITE;
   return change_page_attr(pg, 1, prot);
}

int init_module()
{
    // sys_call_table address in System.map
    sys_call_table = (void*)0xc061e4e0;
    original_call = sys_call_table[__NR_open];

    set_page_rw(sys_call_table);
    sys_call_table[__NR_open] = our_sys_open;
}

void cleanup_module()
{
   // Restore the original call
   sys_call_table[__NR_open] = original_call;
}

As others have hinted, the whole story is a bit different now on modern kernels. I'll be covering x86-64 here, for syscall hijacking on modern arm64 refer to this other answer of mine. Also NOTE: this is plain and simple syscall hijacking. Non-invasive hooking can be done in a much nicer way using kprobes.

Since Linux v4.17, x86 (both 64 and 32 bit) now uses syscall wrappers that take a struct pt_regs * as the only argument (see commit 1, commit 2). You can see arch/x86/include/asm/syscall.h for the definitions.

Additionally, as others have described already in different answers, the simplest way to modify sys_call_table is to temporarily disable CR0 WP (Write-Protect) bit, which could be done using read_cr0() and write_cr0(). However, since Linux v5.3, [native_]write_cr0 will check sensitive bits that should never change (like WP) and refuse to change them (commit). In order to work around this, we need to write CR0 manually using inline assembly.

Here is a working kernel module (tested on Linux 5.10 and 5.18) that does syscall hijacking on modern Linux x86-64 considering the above caveats and assuming that you already know the address of sys_call_table (if you also want to find that in the module, see Proper way of getting the address of non-exported kernel symbols in a Linux kernel module):

// SPDX-License-Identifier: (GPL-2.0 OR MIT)
/**
 * Test syscall table hijacking on x86-64. This module will replace the `read`
 * syscall with a simple wrapper which logs every invocation of `read` using
 * printk().
 *
 * Tested on Linux x86-64 v5.10, v5.18.
 *
 * Usage:
 *
 *     sudo cat /proc/kallsyms | grep sys_call_table # grab address
 *     sudo insmod syscall_hijack.ko sys_call_table_addr=0x<address_here>
 */

#include <linux/init.h>          // module_{init,exit}()
#include <linux/module.h>        // THIS_MODULE, MODULE_VERSION, ...
#include <linux/kernel.h>        // printk(), pr_*()
#include <asm/special_insns.h>   // {read,write}_cr0()
#include <asm/processor-flags.h> // X86_CR0_WP
#include <asm/unistd.h>          // __NR_*

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

typedef long (*sys_call_ptr_t)(const struct pt_regs *);

static sys_call_ptr_t *real_sys_call_table;
static sys_call_ptr_t original_read;

static unsigned long sys_call_table_addr;
module_param(sys_call_table_addr, ulong, 0);
MODULE_PARM_DESC(sys_call_table_addr, "Address of sys_call_table");

// Since Linux v5.3 [native_]write_cr0 won't change "sensitive" CR0 bits, need
// to re-implement this ourselves.
static void write_cr0_unsafe(unsigned long val)
{
    asm volatile("mov %0,%%cr0": "+r" (val) : : "memory");
}

static long myread(const struct pt_regs *regs)
{
    pr_info("read(%ld, 0x%lx, %lx)\n", regs->di, regs->si, regs->dx);
    return original_read(regs);
}

static int __init modinit(void)
{
    unsigned long old_cr0;

    real_sys_call_table = (typeof(real_sys_call_table))sys_call_table_addr;

    pr_info("init\n");

    // Temporarily disable CR0 WP to be able to write to read-only pages
    old_cr0 = read_cr0();
    write_cr0_unsafe(old_cr0 & ~(X86_CR0_WP));

    // Overwrite syscall and save original to be restored later
    original_read = real_sys_call_table[__NR_read];
    real_sys_call_table[__NR_read] = myread;

    // Restore CR0 WP
    write_cr0_unsafe(old_cr0);
    pr_info("init done\n");

    return 0;
}

static void __exit modexit(void)
{
    unsigned long old_cr0;

    pr_info("exit\n");

    old_cr0 = read_cr0();
    write_cr0_unsafe(old_cr0 & ~(X86_CR0_WP));

    // Restore original syscall
    real_sys_call_table[__NR_read] = original_read;

    write_cr0_unsafe(old_cr0);

    pr_info("goodbye\n");
}

module_init(modinit);
module_exit(modexit);
MODULE_VERSION("0.1");
MODULE_DESCRIPTION("Test syscall table hijacking on x86-64.");
MODULE_AUTHOR("Marco Bonelli");
MODULE_LICENSE("Dual MIT/GPL");
Related