Linux内核:系统调用挂钩示例

78

我正在尝试编写一些简单的测试代码,以演示如何钩取系统调用表。

"sys_call_table" 在2.6中不再被导出,所以我只能从 System.map 文件中获取地址。经过查看,我可以确认这个地址是正确的(通过查看我找到的地址处的内存,我可以看到指向系统调用的指针)。

然而,当我尝试修改这个表时,内核会报错并显示 "unable to handle kernel paging request at virtual address c061e4f4",接着机器就会重新启动。

这是运行2.6.18-164.10.1.el5版本的 CentOS 5.4。是否存在某种保护措施,或者我只是遇到了一个 bug?我知道它带有 SELinux,并已尝试将其设置为宽容模式,但没有任何改变。

这是我的代码:

#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;
}

2
你尝试过使用LD_PRELOAD或者ptrace吗?它们不能满足你的需求吗? - ezpz
3
不完全是,这个练习的目的是加载一个内核模块,并钩住整个系统的一个系统调用。此时它所做的并不重要。 - Stephen
5
请注意,出于教学目的,查看这个可能是可以的,但它存在技术和许可问题。不要在实际应用中使用! - robert.berger
1
这段代码有哪些使用情况?我能用这种方式钩取任何Linux系统调用吗? - Incerteza
7
@robert.berger,什么?能否详细说明一下? - Tyler
@Tyler,我也是这么想的。我猜测OP正在构建自己的内核以生成系统映射文件。 - sherrellbc
5个回答

67

我最终自己找到了答案。

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

内核在某些时候进行了更改,使得系统调用表变为只读。

cypherpunk:

即使已经晚了,但这个解决方案可能会对其他人有用:在entry.S文件中,您会找到以下代码:

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

sys_call_table是只读的,如果你想“黑客式”地操作sys_call_table,你需要重新编译内核...

该链接还提供了更改内存为可写的示例。

nasekomoe:

大家好。感谢回复。我很久以前就通过修改内存页面的访问权限来解决了这个问题。我已经实现了两个函数,用于我的上层代码:

#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
这是一个经过修改后适用于我的原始代码版本。
#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;
}

3
请注意,在提供的链接中,Linuxerlive声称change_page_attr在内核版本大于2.6.24时将无法使用,因为它已被弃用。 - Stephen
13
记录你解决问题的方案,以供他人参考,点赞加一。 - jgottula
1
注意:当您调用set_memory_rw()并且地址不是页面对齐时,您会收到以下警告:WARNING: at arch/x86/mm/pageattr.c:877 change_page_attr_set_clr+0x343/0x530() (Not tainted)。我正在使用2.6.32版本,仍在解决此问题(因为在调用此函数后,内存似乎仍然是只读的)。 - Corey Henderson
非常棒的回答,非常详细。肯定要点赞。干杯! - A.Smith

27

感谢Stephen,你在这里的研究对我很有帮助。不过,当我尝试在2.6.32内核上尝试时,我遇到了一些问题,并且收到了 WARNING: at arch/x86/mm/pageattr.c:877 change_page_attr_set_clr+0x343/0x530() (Not tainted) 的警告,随后是内核崩溃,无法写入内存地址。

上面提到的行的注释如下:

// People should not be passing in unaligned addresses

下面这段修改后的代码可正常工作:

int set_page_rw(long unsigned int _addr)
{
    return set_memory_rw(PAGE_ALIGN(_addr) - PAGE_SIZE, 1);
}

int set_page_ro(long unsigned int _addr)
{
    return set_memory_ro(PAGE_ALIGN(_addr) - PAGE_SIZE, 1);
}
请注意,即使在某些情况下,这仍然不能实际将页面设置为可读写。在set_memory_rw()内调用的static_protections()函数会删除_PAGE_RW标志,如果:
  • 它位于BIOS区域中
  • 地址位于.rodata内部
  • 已设置CONFIG_DEBUG_RODATA并且内核设置为只读
在解决尝试修改内核函数地址时仍然收到“无法处理内核分页请求”的错误消息后,我发现了这一点。最终,我通过自己查找地址的页表条目并手动将其设置为可写来解决了这个问题。值得庆幸的是,在2.6.26+版本中导出了lookup_address()函数。以下是我编写的代码:
void set_addr_rw(unsigned long addr) {

    unsigned int level;
    pte_t *pte = lookup_address(addr, &level);

    if (pte->pte &~ _PAGE_RW) pte->pte |= _PAGE_RW;

}

void set_addr_ro(unsigned long addr) {

    unsigned int level;
    pte_t *pte = lookup_address(addr, &level);

    pte->pte = pte->pte &~_PAGE_RW;

}

最后,虽然Mark的答案在技术上是正确的,但在Xen中运行时会引起问题。如果你想禁用写保护,请使用读/写cr0函数。我将它们定义为宏:

#define GPF_DISABLE write_cr0(read_cr0() & (~ 0x10000))
#define GPF_ENABLE write_cr0(read_cr0() | 0x10000)

希望这可以帮助其他遇到这个问题的人。


嗨,关于您对Mark答案的评论,只是好奇:在Xen中运行时会引起什么问题? - mrduclaw
3
在我尝试的Xen内核上,它会导致“一般保护故障”。如果你留意一下,Xen定义了自己的xen_write_cr0()函数,它不会禁用写保护,因为虚拟机监控程序会处理这个问题,而客户操作系统没有那种对CPU寄存器的访问权限。 - Corey Henderson
Corey,非常感谢您分享您的发现...希望我能够再点赞100次! - loopforever
你可以在这里找到它:https://github.com/cormander/tpe-lkm/blob/319e1e29ea23055cca1c0a3bce3c865def14d3d2/hijacks.c 请注意,我将这些函数分别提供了两次,以适应不同的内核版本。 - Corey Henderson
谢谢你提供的代码,解决了我的问题。为什么在参数addr中使用了“unsigned long”类型?这会导致很多警告。我使用了“void **”作为addr参数的类型。使用“unsigned long”的特殊原因是什么? - BitSchupser
内存地址是无符号长整型。我没有收到任何编译器警告。 - Corey Henderson

21
请注意,以下方法也可以替代使用change_page_attr,并且不会被弃用:
static void disable_page_protection(void) {

    unsigned long value;
    asm volatile("mov %%cr0,%0" : "=r" (value));
    if (value & 0x00010000) {
            value &= ~0x00010000;
            asm volatile("mov %0,%%cr0": : "r" (value));
    }
}

static void enable_page_protection(void) {

    unsigned long value;
    asm volatile("mov %%cr0,%0" : "=r" (value));
    if (!(value & 0x00010000)) {
            value |= 0x00010000;
            asm volatile("mov %0,%%cr0": : "r" (value));
    }
}

2
这里做什么样的Vodou?0x00010000咒语召唤哪个Loa精灵? - osgx
8
cr0是一个控制寄存器。第16位控制页面保护强制执行 - 切换它,突然"只读"页面不再重要。你可以在内核空间中执行此操作,因为代码被标记为特权级别(环)0。普通程序无法对自身执行此操作。所以基本上,关闭写保护,在"只读"内存上践踏,然后再次打开,就完成了。你不能淘汰这个功能,因为它属于内核设计的一部分,由于内核是单片式的,所有模块都在环0中运行。 - user257111
3
如果你要这样做,你应该在修改cr0之前禁用中断——使用cli,完成后重新启用中断——使用sti。详情请参见http://vulnfactory.org/blog/2011/08/12/wp-safe-or-not/。 - mttrb
修改cr0的方式是否意味着它正在操作当前页面? - sherrellbc
如果您修改了CR0,它将计入CPU,因此当CPU被禁用时,所有指令都将禁用这些保护,而不管地址如何。(https://en.wikipedia.org/wiki/Control_register) - user7296055

16

如果你正在处理3.4及以上版本的内核(它也可以在较早的内核上工作,但我没有测试),我建议使用更智能的方法来获取系统调用表位置。

例如

#include <linux/module.h>
#include <linux/kallsyms.h>

static unsigned long **p_sys_call_table;
/* Aquire system calls table address */
p_sys_call_table = (void *) kallsyms_lookup_name("sys_call_table");

就是这样,没有地址问题,它可以与我测试过的每个内核正常工作。

您可以使用相同的方式从您的模块中使用未导出的内核函数:

static int (*ref_access_remote_vm)(struct mm_struct *mm, unsigned long addr,
                void *buf, int len, int write);
ref_access_remote_vm = (void *)kallsyms_lookup_name("access_remote_vm");

享受吧!


kallsyms_lookup_name会搜索代码和数据段吗? - ransh
1
哼,我以为只有在编译内核时在.config中设置了KALLSYMS_ALL=yes才能实现这一点。如果/proc/kallsyms中没有该符号,我不知道它是否有效。 - perror
1
在互联网上所有的答案中,只有这个对我起作用了!从System.map复制sys_call_table的地址会在内核中生成一个页面错误oops。 - skrtbhtngr

0

正如其他人所暗示的那样,现代内核的整个故事现在有些不同。 我将在这里涵盖x86-64,对于现代arm64上的系统调用劫持,请参阅我的另一个答案。 另外注意:这是简单的系统调用劫持。可以使用kprobes以更好的方式进行非侵入式钩取。

自从 Linux v4.17 开始,x86(包括 64 位和 32 位)现在使用系统调用包装器,该包装器将 struct pt_regs * 作为唯一参数(参见 commit 1commit 2)。你可以查看 arch/x86/include/asm/syscall.h 来获取定义。

此外,正如其他答案中已经描述的那样,修改sys_call_table最简单的方法是暂时禁用CR0 WP(写保护)位,这可以使用read_cr0()write_cr0()来完成。然而,自Linux v5.3以来,[native_]write_cr0将检查不应更改的敏感位(如WP)并拒绝更改它们(commit)。为了解决这个问题,我们需要使用内联汇编手动编写CR0。
以下是一个可工作的内核模块(在Linux 5.10和5.18上测试过),考虑到上述注意事项并假设您已经知道sys_call_table的地址(如果您还想在模块中找到它,请参见Proper way of getting the address of non-exported kernel symbols in a Linux kernel module)进行现代Linux x86-64系统调用劫持。
// 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");

它在我的Android 12 Linux内核4.14.180上无法工作。Marco,我在Facebook上向您发送了好友请求。 - Zibri
@Zibri 这是针对5.10 x86-64的,我没有在旧内核上测试过,也没有在ARM / ARM64上测试过。如果您遇到问题,应该发布一个问题并详细说明您的问题。 - Marco Bonelli

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接