Linux Kernel 4.2.x:为啥检查时预期的系统调用地址与实际地址不匹配?

Posted

技术标签:

【中文标题】Linux Kernel 4.2.x:为啥检查时预期的系统调用地址与实际地址不匹配?【英文标题】:Linux Kernel 4.2.x: Why does the expected system call address not match the actual address when checked?Linux Kernel 4.2.x:为什么检查时预期的系统调用地址与实际地址不匹配? 【发布时间】:2016-02-03 13:32:55 【问题描述】:

短背景

我目前正在编写一个 linux 内核模块作为一个项目,以更好地了解 linux 内核内部结构。我以前写过'hello world'类型的模块,但我想超越它,所以我试图用openreadwriteclose替换一些常见的系统调用我自己的,以便我可以print 将更多信息写入系统日志。

我在搜索时发现的一些内容是 2.6 之前的内核,这没有用,因为从内核 2.6.x 开始,sys_call_table 符号停止导出。另一方面,我为 2.6.x 或更高版本找到的那些似乎有其自身的问题,即使它们当时显然有效。

我在sys_call_table in linux kernel 2.6.18 帖子上发现了一个特别的O'Reilly article,它表明我正在尝试做的事情应该有效,但事实并非如此。 (具体请参见Intercepting sys_unlink() Using System.map 部分。)

我还阅读了Linux Kernel: System call hooking example 和Kernel sys_call_table address does not match address specified in system.map,虽然有些信息量很大,但对我没有用。

问题和疑问

第 1 部分 - 意外的地址不匹配

我在 Kubuntu 15.10 x86_64 架构安装上使用 Linux 内核 4.2.0-16-generic。由于sys_call_table符号不再导出,我grepped从系统映射文件中获取地址:

# grep 'sys_call_table' < System.map-4.2.0-16-generic
ffffffff818001c0 R sys_call_table
ffffffff81801580 R ia32_sys_call_table

有了这个,我在我的内核模块中添加了以下行:

static unsigned long *syscall_table = (unsigned long *) 0xffffffff818001c0;

基于此,我期待一个简单的检查实际上会确认我实际上指向的位置是我认为我所指向的位置,即内核未导出的sys_call_table 的基地址.所以,我在模块的 init 函数中写了一个类似下面的简单检查来验证:

if(syscall_table[__NR_close] != (unsigned long *)sys_close)

        pr_info("sys_close = 0x%p, syscall_table[__NR_close] = 0x%p\n", sys_close, syscall_table[__NR_close]);
        return -ENXIO;

此检查失败,日志中打印了不同的地址。

期待这个if语句的主体被执行,因为我认为syscall_table[__NR_close]返回的地址与sys_close的地址相同,但它确实进入。

Q1:到目前为止,我是否遗漏了一些关于预期的基于地址的比较?如果有,是什么?

第 2 部分 - 部分成功?

如果我删除此检查,我似乎部分成功了,因为显然,我至少可以使用以下代码成功替换 read 调用:

static asmlinkage ssize_t (*original_read)(unsigned int fd, char __user *buf, size_t count);
// ...
static void systrap_replace_syscalls(void)

    pr_debug("systrap: replacing system calls\n");

    original_read  = syscall_table[__NR_read];
    original_write = syscall_table[__NR_write];
    original_close = syscall_table[__NR_close];

    write_cr0(read_cr0() & ~0x10000);

    syscall_table[__NR_read]  = systrap_read;
    syscall_table[__NR_write] = systrap_write;
    syscall_table[__NR_close] = systrap_close;

    write_cr0(read_cr0() | 0x10000);

    pr_debug("systrap: system calls replaced\n");

我的替换函数只是打印一条消息并将调用转发到实际的系统调用。例如读取替换函数的代码如下:

static asmlinkage ssize_t systrap_read(unsigned int fd, char __user *buf, size_t count)

        pr_debug("systrap: reading from fd = %u\n", fd);
        return original_read(fd, buf, count);

当我insmodrmmod模块时,系统日志显示以下输出:

kernel: [23226.797460] systrap: setting up module
kernel: [23226.797462] systrap: replacing system calls
kernel: [23226.797464] systrap: system calls replaced
kernel: [23226.797465] systrap: module setup complete
kernel: [23226.864198] systrap: reading from fd = 4279272912

<similar output ommitted for brevity>

kernel: [23235.560663] systrap: reading from fd = 2835745072
kernel: [23235.564774] systrap: reading from fd = 861079840
kernel: [23235.564986] systrap: cleaning up module
kernel: [23235.564990] systrap: trying to restore system calls
kernel: [23235.564993] systrap: restored sys_read
kernel: [23235.564995] systrap: restored sys_write
kernel: [23235.564997] systrap: restored sys_close
kernel: [23235.565000] systrap: system call restoration attempt complete
kernel: [23235.565002] systrap: module cleanup complete

我可以让它运行很长时间,而且奇怪的是,我从不观察到 writeclose 函数调用的条目 -- 仅适用于 reads,它这就是为什么我认为我只是部分成功了。

Q2:我是否遗漏了一些关于替换系统调用的内容?如果有,是什么?

第 3 部分 - rmmod 命令出现意外错误消息

即使模块看起来运行正常,当我从内核rmmod 模块时,我总是收到以下错误:

rmmod: ERROR: ../libkmod/libkmod.c:506 lookup_builtin_file() could not open builtin file '(null)/modules.builtin.bin'

我的模块清理函数只是调用另一个(如下),它试图通过执行与上述替换函数相反的操作来恢复函数调用:

// called by the exit function
static void systrap_restore_syscalls(void)

    pr_debug("systrap: trying to restore system calls\n");
    write_cr0(read_cr0() & ~0x10000);

    /* make sure no other modules have made changes before restoring */
    if(syscall_table[__NR_read] == systrap_read)
    
            syscall_table[__NR_read] = original_read;
            pr_debug("systrap: restored sys_read\n");
    
    else
    
            pr_warn("systrap: sys_read not restored; address mismatch\n");
    
    // ... ommitted: same stuff for other sys calls

    write_cr0(read_cr0() | 0x10000);
    pr_debug("systrap: system call restoration attempt complete\n");

Q3:不知道是什么原因导致的错误信息;这里有什么想法吗?

第 4 部分 - sys_open 标记为弃用?

在另一个意外的转折中,我发现__NR_open 宏不再默认定义。为了让我看到定义,我必须在 #includeing 头文件之前 #define __ARCH_WANT_SYSCALL_NO_AT

/*
 * Force __NR_open definition. It seems sys_open has been replaced by sys_openat(?)
 * See include/uapi/asm-generic/unistd.h:724-725
 */
#define __ARCH_WANT_SYSCALL_NO_AT

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
// ...

通过内核源代码(在上面的评论中提到),您会发现以下 cmets:

/*
* All syscalls below here should go away really,
* these are provided for both review and as a porting
* help for the C library version.
*
* Last chance: are any of these important enough to
* enable by default?
*/
#ifdef __ARCH_WANT_SYSCALL_NO_AT
#define __NR_open 1024
__SYSCALL(__NR_open, sys_open)
// ...

谁能澄清一下:

Q4:...上面的cmets关于为什么__NR_open默认不可用?,

Q5:...使用#define 做我正在做的事情是否是个好主意?以及

Q6:...如果我真的不应该尝试使用__NR_open,我应该使用什么?

Epiloge - 让我的系统崩溃???

我尝试使用__NR_openat,将那个调用替换为之前的调用:

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

    pr_debug("systrap: opening file dfd = %d, name = % s\n", filename);
    return original_openat(dfd, filename, flags, mode);

但这只是帮助我毫不客气地让我自己的系统崩溃????通过导致其他进程在尝试打开文件时出现段错误,例如:

kernel: [135489.202693] systrap: opening file dfd = 0, name = P^Q
kernel: [135489.202913] zsh[11806]: segfault at 410 ip 00007f3a380abe60 sp 00007ffd04c5b550 error 4 in libc-2.21.so[7f3a37fe1000+1c0000]

尝试打印参数数据也显示奇怪/垃圾信息。

Q7:关于为什么它会突然崩溃以及为什么这些论点看起来像垃圾一样的任何其他建议?

我已经花了几天的时间试图解决这个问题,我只是希望我没有错过一些完全愚蠢的事情......

如果您对 cme​​ts 有什么不完全清楚的地方,请告诉我,我会尽力澄清。

如果您能提供一些实际工作的代码 sn-ps 和/或为我指出一个足够精确的方向,让我了解我做错了什么以及如何快速解决这个问题,我将非常有帮助固定。

【问题讨论】:

Q1:您已阅读问题“内核 sys_call_table 地址不匹配 system.map 中指定的地址”,但仍为此使用System.map目的。好的! Q4openat 在以AT_FDCWD 作为第一个参数调用时提供open 的功能,因此不需要openQ5:在模块代码中定义__ARCH_WANT_SYSCALL_NO_AT 绝对是个坏主意。这个宏描述了整个架构,而不是一个模块。此外,作为一项规则,几乎每个由内核包含检查的宏都只能在内核中定义,而不是由模块定义。 @Tsyvarev:“不过还是使用 System.map 来达到这个目的。太好了!”好吧,我与之交谈过的其他人也在做类似的事情,而且对他们来说似乎效果很好。链接的 O'Reilly 文章也建议使用这种方法,特别是在 intercepting sys_unlink using System.map 上。你没看过吗? O'Reilly 文章的内核版本早于 3.13,用于地址不一致的问题。您的内核(4.2.)甚至更新。此外,您从System.map 获得的地址与问题中的地址相似。为什么不使用其他方法检查表地址的有效性? @Tsyvarev:我的理解是,只要它是 2.6 后的内核,它的工作方式应该与我上面尝试的类似。如果这不是真的(不再是?),请详细说明。另外,我不确定 64 位系统的内核内存范围是多少(我发现似乎没有定论),但我也想避免像一些使用 32 位内核的文章在模块初始化期间那样扫描内存范围。那么,您会建议什么“其他方法”? My understanding is that, as long as it's a post 2.6 kernel, it's supposed to work in a similar manner to what I'm trying above. - 不,即使在 2.6 之后,Linux 内核也会继续积极变化。我知道一些与符号可见性有关的事情,这在 2.6.32 和 3.10 之间已更改。我建议将扫描作为另一种方法,在关于地址不一致的问题中,提问者已使用此方法(在 x86_64 上!)。该方法不需要是您的最终决定,仅用于检查。 【参考方案1】:

我已经完成了这项工作,现在我正在花时间记录我的发现。

Q1:关于预期的基于地址的比较,我是否遗漏了什么?

这个比较的问题是,在检查/proc/kallsyms之后,我看到sys_close和其他相关符号也不再导出。对于某些符号,我已经知道这一点,但我仍然(错误地)认为其他一些符号仍然可用。所以我使用的检查(如下)评估为真,并导致模块未能通过“安全”检查。

if(syscall_table[__NR_close] != (unsigned long *)sys_close)

        /* ... */

简而言之,您只需要相信关于从System.map-$(uname -r) 文件中检索到的系统调用表地址的假设。 “安全”检查是不必要的,也不会按预期工作。

Q2:我是否遗漏了有关被替换的系统调用的某些内容?

这个问题最终追溯到我包含的以下一个或两个头文件(我没有费心去弄清楚是哪一个。):

#include <uapi/asm-generic/unistd.h>
#include <uapi/asm-generic/errno-base.h>

这些导致__NR_* 宏被重新定义,因此扩展为不正确的值——至少对于 x86_64 架构。例如,系统调用表中sys_readsys_write 的索引应该分别为01,但它们得到了其他值并最终索引到表中完全意外的位置。

只需删除上面的头文件即可解决问题,无需额外的代码更改。

Q3:不知道是什么原因导致的错误信息;这里有什么想法吗?

错误消息是上一期的副作用。显然,系统调用表的索引不正确(参见Q2)导致内存中的其他位置被修改。

Q4:...上面的 cmets 关于为什么 __NR_open 默认不可用?

这是我停止使用的 IDE 的错误报告。 __NR_open 宏已经定义; Q2 的修复使其更加明显。

Q5:...用#define 做我正在做的事情是否是个好主意?

简短回答:不,这不是一个好主意,绝对不需要。请参阅上面的Q2

Q6:...如果我真的不应该尝试使用__NR_open,我应该使用什么来代替@

根据对之前问题的回答,这不是问题。使用__NR_open 很好,也符合预期。由于Q2

中的头文件,这部分已经搞砸了

Q7:关于为什么它会突然崩溃以及为什么论点看起来像垃圾一样的任何其他建议?

__NR_openat 的使用和崩溃很可能是由于宏被扩展为不正确的值(再次参见 Q2)。但是,我可以说我没有真正需要使用它。我应该使用上面指定的 __NR_open,但正在尝试使用 __NR_openat 作为解决Q2中修复的问题的解决方法。

简而言之,Q2 的答案有助于解决级联效应中的几个问题。

【讨论】:

以上是关于Linux Kernel 4.2.x:为啥检查时预期的系统调用地址与实际地址不匹配?的主要内容,如果未能解决你的问题,请参考以下文章

Linux系统移植:Kernel 顶层 Makefile(上)

Linux系统之升级内核版本方法

使用 NSPersistentCloudKitContainer 时预填充核心数据存储的最佳方法是啥?

为啥 Kernel#require 在 Ruby 中引发 LoadError?

Linux内核启动流程 详解

在应用程序启动时预加载 UITabBar 选项卡时出现问题(崩溃)