【问题标题】:Linux Kernel: System call hooking exampleLinux 内核:系统调用挂钩示例
【发布时间】:2026-02-24 07:25:02
【问题描述】:

我正在尝试编写一些简单的测试代码作为挂钩系统调用表的演示。

“sys_call_table”在2.6中不再导出,所以我只是从System.map文件中抓取地址,我可以看到它是正确的(在我找到的地址处查看内存,我可以看到指向系统调用的指针)。

但是,当我尝试修改此表时,内核会给出“糟糕”提示“无法在虚拟地址 c061e4f4 处理内核分页请求”,然后机器会重新启动。

这是运行 2.6.18-164.10.1.el5 的 CentOS 5.4。有某种保护还是我只是有一个错误?我知道它是 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;
}

【问题讨论】:

  • 你试过LD_PRELOADptrace吗?他们不满足你想要做的事情吗?
  • 并非如此,练习的目的是加载一个内核模块,该模块将挂钩整个系统的系统调用。那时它做了什么并不重要。
  • 请注意,出于教学目的,可能可以对此进行研究,但它存在技术和许可问题。不要在现实世界中使用它!
  • 这段代码有什么用例?我可以用这种方式挂钩任何 linux 系统调用吗?
  • @robert.berger,什么?想扩大一点吗?

标签: c linux-kernel hook


【解决方案1】:

我终于自己找到了答案。

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

内核在某些时候发生了变化,因此系统调用表是只读的。

密码朋克:

即使迟到了,但解决方案 其他人也可能感兴趣:在 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;
}

【讨论】:

  • 请注意,在提供的链接中,Linuxerlive 声称 change_page_attr 不适用于内核 > 2.6.24,因为它已经过时了。
  • +1 用于记录您为其他人看到的解决方案。
  • 请注意,当您调用 set_memory_rw() 并且地址不是页面对齐时,您会得到: WARNING: at arch/x86/mm/pageattr.c:877 change_page_attr_set_clr+0x343/0x530( )(未污染)。我正在使用 2.6.32,仍在制定解决方案(因为在我调用它之后内存似乎仍然是只读的)。
  • 对您自己的问题的精彩回答。非常详细。 +1 肯定。干杯人。
【解决方案2】:

感谢斯蒂芬,您在这里的研究对我很有帮助。不过,当我在 2.6.32 内核上尝试此操作时,我遇到了一些问题,并收到 WARNING: at arch/x86/mm/pageattr.c:877 change_page_attr_set_clr+0x343/0x530() (Not tainted) 后跟内核 OOPS,因为无法写入内存地址。

上述行上方的评论指出:

// 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 已设置,内核设置为只读

我在调试后发现了为什么在尝试修改内核函数的地址时仍然“无法处理内核分页请求”。我最终能够通过自己找到地址的页表条目并手动将其设置为可写来解决该问题。幸运的是,lookup_address() 函数在 2.6.26+ 版本中导出。这是我为此编写的代码:

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 中运行时会导致什么问题?
  • 在我尝试过的 xen 内核上,它会导致“一般保护错误”。如果您注意到,xen 定义了它自己的 xen_write_cr0() 函数,它不会禁用写保护,因为虚拟机管理程序会处理它,而客户操作系统没有对 CPU 寄存器的那种访问权限。
  • Corey,非常感谢您分享您的发现...希望我能再投票 100 次!
  • 你可以在这里找到它:github.com/cormander/tpe-lkm/blob/… 请注意,我有两次这些功能,用于不同的内核版本。
  • 感谢代码,解决了我的问题。为什么参数 addr 使用unsigned long。这会导致很多警告。我使用 void** 作为 addr 参数的类型。使用unsigned long有什么特殊原因吗?
【解决方案3】:

请注意,以下内容也可以代替使用 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));
    }
}

【讨论】:

  • 这里做了什么伏都? 0x00010000咒语叫什么神灵?
  • @osgx cr0 是一个控制寄存器。第 16 位控制页面保护强制执行 - 切换它,突然页面“只读”不再重要。您可以在内核空间中执行此操作,因为代码被标记为特权级别(环)0。普通程序无法对自己执行此操作。所以基本上,关闭写保护,践踏“只读”内存,再次打开它,瞧。您不能弃用它,因为它是内核设计的一部分,是单片的,所有模块都在 ring 0 中运行。
  • 如果您打算这样做,您应该在修改cr0 之前禁用中断cli,并在完成后重新启用中断sti。详情请见vulnfactory.org/blog/2011/08/12/wp-safe-or-not
  • 这样修改cr0是否意味着作用于当前页面?
  • 如果你修改 cr0 它对 cpu 计数,所以当它被禁用时,cpu 上的所有指令都将禁用这些保护,无论地址如何。 (en.wikipedia.org/wiki/Control_register)
【解决方案4】:

如果您正在处理内核 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 会在代码段和数据段中搜索吗?
  • 嗯,我认为只有在编译内核时你的.config 中有KALLSYMS_ALL=yes 才有可能。如果/proc/kallsyms 中没有符号,我不知道它是否有效。
  • 在互联网上的所有答案中,只有这个对我有用!从 System.map 复制 sys_call_table 的地址会在 Kernel 中生成页面错误 oops。