系统调用挂钩示例参数不正确

Posted

技术标签:

【中文标题】系统调用挂钩示例参数不正确【英文标题】:System call hooking example arguments are incorrect 【发布时间】:2022-01-07 02:44:12 【问题描述】:

我从我们的 Linux 内核模块中编写了一个系统调用挂钩示例。

更新了系统调用表中的开放系统调用以使用我的入口点而不是默认入口点。

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

MODULE_LICENSE("GPL");
char *sym_name = "sys_call_table";

typedef asmlinkage long (*sys_call_ptr_t)(const struct pt_regs *);
static sys_call_ptr_t *sys_call_table;
typedef asmlinkage long (*custom_open) (const char __user *filename, int flags, umode_t mode);


custom_open old_open;

static asmlinkage long my_open(const char __user *filename, int flags, umode_t mode)

    char user_msg[256];
    pr_info("%s\n",__func__);
    memset(user_msg, 0, sizeof(user_msg));
    long copied = strncpy_from_user(user_msg, filename, sizeof(user_msg));
    pr_info("copied:%ld\n", copied);
    pr_info("%s\n",user_msg);

    return old_open(filename, flags, mode);




static int __init hello_init(void)

    sys_call_table = (sys_call_ptr_t *)kallsyms_lookup_name(sym_name);
    old_open = (custom_open)sys_call_table[__NR_open];
    // Temporarily disable write protection
    write_cr0(read_cr0() & (~0x10000));
    sys_call_table[__NR_open] = (sys_call_ptr_t)my_open;
    // Re-enable write protection
    write_cr0(read_cr0() | 0x10000);

    return 0;


static void __exit hello_exit(void)

    // Temporarily disable write protection
    write_cr0(read_cr0() & (~0x10000));
    sys_call_table[__NR_open] = (sys_call_ptr_t)old_open;
    // Re-enable write protection
    write_cr0(read_cr0() | 0x10000);



module_init(hello_init);
module_exit(hello_exit);

我写了一个简单的用户程序来验证。

#define _GNU_SOURCE
#include <sys/syscall.h>
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>

int main(int argc, char *argv[])

    int fd = syscall(__NR_open, "hello.txt", O_RDWR|O_CREAT, 0777);

    exit(EXIT_SUCCESS);

在我的文件夹中创建了文件,但 strncpy_user 因地址错误而失败

[  927.415905] my_open
[  927.415906] copied:-14

上面的代码有什么错误?

【问题讨论】:

风格:你为什么想要一个非const/非静态全局char *sym_name?此外,char user_msg[256] = 0 会比 memset 更容易。同样对于一次性实验,我可能只是假设系统调用表条目是sys_open,而不是保存旧指针。 (由于某种原因,非静态变量。) strncpy_from_user 返回的 -14-EFAULT。你确定你的 syscall 包装器实际上是在传递一个指向有效字符串的指针吗?检查strace 来自 strace:open("hello.txt", O_RDWR|O_CREAT, 0777) = 3 。文件创建成功 哦,还有一个潜在的缓冲区超读错误:如果源字符串太大而无法容纳,strncpy 不会 0 终止目标。提前清零缓冲区是没有用的;相反,您需要检查返回值是否为非错误且 user_msg[sizeof(user_msg)-1] = 0 始终将最后一个字节归零,复制之前的第一个字节。由于它很小并且在堆栈上,所以这对性能来说甚至还不错。 你用不同的签名的函数替换原来的函数一个签名,然后问为什么你得到了错误的参数?真的吗?显式转换为 (sys_call_ptr_t) 用于克服编译器的警告/错误是“出现问题”的直接信号。 【参考方案1】:

OP 可能正在使用使用“系统调用包装器”的内核/架构,其中系统调用表包含一个调用真正的系统调用函数(可能作为内联函数调用)的包装器函数。 x86_64 架构从内核版本 4.17 开始就使用系统调用包装器。

对于内核 4.17 或更高版本上的 x86_64,sys_call_table[__NR_open] 指向 __x64_sys_open(带有原型 asmlinkage long __x64_sys_open(const struct pt_regs *regs)),它调用 static 函数 __se_sys_open(带有原型 static long __se_sys_open(const __user *filename, int flags, umode_t mode)),它调用内联函数 @987654329 @(带有原型static inline long __do_sys_open(const __user *filename, int flags, umode_t mode)。这些都将由“fs/open.c”中的SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)宏调用和宏调用之后的函数体定义。

SYSCALL_DEFINE3 在“include/linux/syscalls.h”中定义,并在同一文件中使用SYSCALL_DEFINEx 宏,该文件使用__SYSCALL_DEFINEx 宏。由于 x86_64 定义了CONFIG_ARCH_HAS_SYSCALL_WRAPPER,所以__SYSCALL_DEFINEx 宏由#include &lt;asm/syscall_wrapper.h&gt; 定义,映射到“arch/x86/include/asm/syscall_wrapper.h”。


有关此更改的背景,请参阅

LWN:use struct pt_regs based syscall calling for x86-64 LKML:[PATCH 000/109] 删除对系统调用的内核调用 https://lkml.org/lkml/2018/3/29/409

似乎动机是只传递一个指向pt_regs 的指针,而不是在调用链下游的寄存器中有一堆用户空间值。 (也许是通过降低小工具的用处来增强对 Spectre 攻击的抵抗力?)


为什么 open 仍然有效,即使包装器没有

如果 OP 确实使用 x86_64 内核 4.17 或更高版本,并将 sys_call_table[__NR_open] 条目替换为指向使用不同原型并使用相同参数调用原始函数(由 old_open 指向)的函数的指针,这就解释了为什么对strncpy_from_user(user_msg, filename, sizeof(user_msg)) 的调用失败了。虽然声明为const char * __user filename,但filename指针实际上指向的是内核空间中原来的struct pt_regs

在随后对old_open(filename, flags, mode) 的调用中,第一个参数filename 仍指向原始struct pt_regs,因此旧函数(需要struct pt_regs * 类型的单个参数)仍按预期工作。

即该函数在其第一个指针 arg 上未更改,尽管调用它的类型不同。

【讨论】:

感谢您的详细解释。我清楚地理解我犯的错误。那么,函数参数的获取方式是什么呢? 您能否提供更多关于谁实际将 pt_regs 映射到系统调用处理程序参数的信息【参考方案2】:

更新:以下是工作代码,感谢大家提供输入

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

    MODULE_LICENSE("GPL");
    char *sym_name = "sys_call_table";

    typedef asmlinkage long (*sys_call_ptr_t)(const struct pt_regs *);
    static sys_call_ptr_t *sys_call_table;


    sys_call_ptr_t old_open;

    static asmlinkage long my_open(const struct pt_regs *regs)
    
        char __user *filename = (char *)regs->di;
            char user_filename[256] = 0;
        long copied = strncpy_from_user(user_filename, filename, sizeof(user_filename));
        if (copied > 0)
            pr_info("%s filename:%s\n",__func__, user_filename);
        return old_open(regs);
    



    static int __init hello_init(void)
    
        sys_call_table = (sys_call_ptr_t *)kallsyms_lookup_name(sym_name);
        old_open = sys_call_table[__NR_open];
        // Temporarily disable write protection
        write_cr0(read_cr0() & (~0x10000));
        sys_call_table[__NR_open] = my_open;
        // Re-enable write protection
        write_cr0(read_cr0() | 0x10000);

        return 0;
    

    static void __exit hello_exit(void)
    
        // Temporarily disable write protection
        write_cr0(read_cr0() & (~0x10000));
        sys_call_table[__NR_open] = old_open;
        // Re-enable write protection
        write_cr0(read_cr0() | 0x10000);

    

    module_init(hello_init);
    module_exit(hello_exit);

【讨论】:

自己实现调用约定(从 RDI 获取第一个 arg)有点小技巧,但是是的,这可能比让内核宏为您生成包装器更容易。您仍然暴露了将可能未终止的缓冲区传递给%s 格式字符串的相同错误;也许使用%.256s 来设置最大值。在将完整大小传递给strncpy 之前,= 0 初始化整个数组是没有意义的;这只是浪费了归零工作。

以上是关于系统调用挂钩示例参数不正确的主要内容,如果未能解决你的问题,请参考以下文章

VB 函数调用中参数的正确语法

Swift 函数调用列表不正确的参数类型

尽管函数调用的参数数量不正确,但 C 程序编译

对本机函数“DATEDIFF”的调用中的参数计数不正确

使用不正确数量的参数调用 Cmake add_executable [重复]

如何在 Parse Cloud Code 和客户端之间正确传递参数? (httpRequest 示例)