微信号:LinuxKernelTravel

介绍:Linux内核之旅

Linux系统调用日志收集程序_x86_64环境3.14版本内核

2015-11-25 13:22 何兴鹏

题记:学习Linux内核的麻烦在于版本的不断更新,有趣也在于在每个新版本中都有新的东西出现,你不得不做调整和改变。重新编译内核,增加新的东西,适应新的版本,如此,旧瓶中的新酒就有了味道-- 陈老师


1 简介

1.1 背景

系统调用是用户程序与系统打交道的唯一入口,因此系统调用的安全直接关系到系统的安全。如果一个用户恶意地不断调用fork()将导致系统负载增加,因此我们有必要收集一些有危险的系统调用记录,将有利于系统管理进行事后追踪,从而提高系统的安全性。

1.2 原理

前提:本程序在x86_64构架ubuntu14.04麒麟系统上测试通过。系统自带内核版本为3.13,实验使用在www.kernel.org下载的3.14.57版本内核。

源程序是《linux操作系统原理与应用 第二版》– (陈莉君 康华 编著) 中的第6章程序,本文对其进行修改以适配64位系统和3.14内核。

系统调用日志收集程序结构图如下:

  • 当用户态程序调用fork,open函数时,在系统调用入口system_call会拦截到此次调用并调用sys_syscall_audit函数,记录此次调用。这里只列举了两个系统调用,程序中还拦截了close,execv,getpid,clone调用。

  • 当myaudit模块插入内核时,syscall_audit和sys_audit函数分别被挂在钩子函数my_sysaudit和my_audit上。这样,sys_syscall_audit函数调用钩子函数my_sysaudit时,实际就是调用myaudit模块里的syscall_audit函数,从而对此次拦截的系统调用信息进行记录,存入audit_buf数组里。

  • 当用户运行日记查看程序user_audit时,通过系统调用号317,调用到sys_myaudit函数,通过钩子函数sys_myaudit触发myaudit模块里的sys_audit函数,将日志信息取出。

从结构图中,我们可以总结,程序大致分为四个部分。

  1. 添加两个系统调用,sys_syscall_audit和sys_myaudit,分别用于接收拦截到的系统调用和取出系统调用日志。并且在两个函数里调用钩子函数,以便myaudit模块接入。

  2. 修改系统调用入口system_call,在x86_64下该入口位于arch/x86/kernel/entry_64.S。我们需要在入口添加调用sys_syscall_audit函数来记录系统调用。

  3. myaudit模块的编写,用来处理系统调用日志。

  4. 用户态程序user_audit的编写,将日志信息输出到控制台。

2 准备工作

在编写这个程序之前,从上面总结的四个部分可以知道,我们需要编写用户态程序,内核模块,添加系统调用和修改系统调用入口。
其中添加系统调用和修改系统调用入口意味着我们要修改内核并且重新编译内核,才能使用。所以先介绍内核编译,内核能够编译成功运行之后再修改内核。

2.1 内核编译

2.1.1 下载解压内核

首先可以使用wget命令在kernel.org网站下载linux-3.14.57.tar.xz内核压缩包,之后使用xz -d命令解压得到.tar文件,使用tar -xvf命令解压得到内核文件夹,cd进入内核源代码根目录。
注意:把内核源代码文件夹改名为linux-headers-3.14.57并放在/usr/src/下,这样之后编译模块时就不用更改内核路径了。

2.1.2 简化编译配置

在编译之前,我们需要配置编译参数,标记需要哪些模块,哪些功能。
linux内核提供了很多种配置方法,包括make config,make menuconfig,make xconfig等等。

更多详细信息参考Linux内核编译初学者指南

而我们这里使用make localmodconfig进行配置,这是2.6.32版本之后引入的简化内核的配置方式。该方式仅编译当前内核已加载的模块,没有使用的模块则不编译。有些当前内核没有的特性会提示选择,我直接回车,因为我这是实验环境,不影响。
对于生产环境,使用localmodconfig配置之后,仍然可以使用make menuconfig来做修改。使用menuconfig之前需要安装ncurses,对于ubuntu可以直接通过
apt-get install ncurses-dev从源安装。
所以配置内核我只用了一步使用
make localmodconfig

题外话:我先前使用make menuconfig默认配置编译内核,结果编译了接近一个小时,并且整个文件夹占用大小有7,8个G。很明显编译了很多不需要的模块及驱动进去,而且还用不了。而使用localmodconfig最简配置之后,配合make -j8并发编译支持,编译一遍内核只需要8分钟。再使用了后面的ccache之后,重新编译只需要不到1分钟。

2.1.3 ccache编译加速

ccache(“compiler cache”的缩写)是一个编译器缓存,该工具会高速缓存编译生成的信息,并在编译的特定部分使用高速缓存的信息。
也就是说ccache会在你重新编译时,只编译修改的部分,相同的部分从缓存读取,这样就大大地加快了重新编译的速度。
ubuntu上可以直接使用
apt-get install ccache通过源安装。
内核编译开启ccache支持,我们只需要修改源代码根目录的Makefile文件。由于内核大部分都是C语言编写,所以只要缓存gcc编译的内容。所以在Makefile文件中找到

HOSTCC = gcc

我们修改成

HOSTCC = ccache gcc

CC = $(CROSS_COMPILE)gcc

改成

CC = ccache $(CROSS_COMPILE)gcc

这样就完成了ccache的配置。

2.1.4 编译

配置了内核文件之后,通常通过make命令来编译内核。也可以把make命令分成两步,make bzImagemake modules分别编译内核和模块驱动。
在多核心的CPU下,我们通过-j参数开启并发任务同时编译。比如我的机器是4核8线程,使用-j8开启8个并发任务。不建议使用make -j不加数字,代表不限制任务数,虚拟机使用该参数导致崩溃。
对于我的情况,编译的命令就是
make -j8

需要重新配置并编译时,先使用make mrproper命令删除之前的临时文件和配置文件。

2.1.5 使用

在当前系统上使用成功编译的内核需要执行make modules_installmake install分别安装模块驱动和内核镜像。在执行make install时,会自动更新grub引导,默认从新的内核镜像启动,并且保留旧的内核引导。

当重新启动系统,发现新内核出错时,可以在开机时,长按shift,进入grub菜单,从更多选项里选择旧的内核启动。

3 程序分析

我们按照之前总结的程序的四个部分依次分析。

3.1 添加两个系统调用

添加系统调用有三步,添加系统调用函数,添加系统调用号,添加声明。

3.1.1 添加系统调用函数

myaudit.c文件


对于系统调用对应的内核函数一定要以sys_开头,我没有用sys_开头时,发现系统调用失败。
对于2.6及以后的内核,如果在其他模块需要使用全局函数,则需要使用EXPORT_SYMBOL导出函数,否则不能使用。但在其他模块使用该函数时编译配置有改动,后面会详细说明。
我将文件myaudit.c放在arch/x86/kernel/myaudit.c。
自己添加的文件需要编译进内核时,可以修改当前目录下的Makefile文件,在相应位置添加,如:

 obj-y += vsmp_64.o obj-y += myaudit.o #添加的行

这样内核就会把myaudit.c文件编译进内核。

3.1.2 添加系统调用号

3.14内核的x86_64系统调用号在arch/x86/syscalls/syscall_64.tbl文件,因为本次实验环境是64位的系统,所以是syscall_64.tbl文件。如果是32位环境则是syscall_32.tbl文件。
我们只需要在最后一个系统调用号下添加即可:

315 common sched_getattr sys_sched_getattr
# 下面是添加的两个系统调用号
316 common mysyscall sys_syscall_audit
317 common myaudit sys_myaudit

3.1.3 添加声明

修改头文件include/linux/syscalls.h,在文件最后添加两个函数声明。

//add myaudit
asmlinkage void sys_syscall_audit(int syscall,int return_status); asmlinkage int sys_myaudit(u8 type, u8 * us_buf, u16 us_buf_size, u8 reset);

3.1.4 编译测试

在添加完两个系统调用之后,编译使用新内核,并对系统调用进行测试。
在内核编译成功,重启之后可以使用
uname -r查看是否是新内核的版本号。
可以编写一个简单的用户态小程序,测试系统调用是否成功。
如:

#include <stdio.h>int main() { syscall(316,11,12); return 0; }

运行之后,通过dmesg命令查看内核输出是否正常。

3.2 修改系统调用入口

3.2.1 说明

在64位的机器上linux内核的系统调用入口是arch/x86/kernel/entry_64.S文件。
这里借用一张系统调用图来说明。

在x86_64构架的系统上,系统调用入口system_call位于entry_64.S。并且当64位系统运行32位程序时,进入ia32entry.S模拟32位环境。可以看到新的内核的系统调用已经不再通过INT 0X80中断进入,通过MSR寄存器。

来源于 [翻譯] 系統呼叫(system call)的剖析(下)

我的位置文件/usr/src/linux-headers-3.14.57/arch/x86/kernel/entry_64.S
在文件中首先找到ENTRY(system_call),即系统调用入口。在入口里找到了两处调用sys_call_table系统调用表的地方,两处是不同情况下的处理。所以,我们对两处都进行拦截。
我们要拦截fork,open,close,execv,getpid,clone这几个调用,先通过之前提到过的syscall_64.tbl文件查看调用号是多少。然后在调用完sys_call_table之后,依次判断调用号是否是这几个,如果是则跳转到myauditsys处理。在myauditsys调用call sys_syscall_audit函数,记录这次系统调用。
在64位构架上,栈顶寄存器由esp变为rsp,存系统调用号和函数返回值的eax变为rax。
具体请看代码注释

参考 调度时机分析之被动调度(之系统调用返回) 调度机时分析之被动调度(之中断处理返回)

3.2.2 代码

参看:http://blog.csdn.net/bboxhe/article/details/50011899

 
Linux内核之旅 更多文章 Linux内核之旅—微信平台开篇 《Linux内核设计与实现》章节节选—操作系统和内核简介 hurlex这个基于x86架构的内核 操作系统中的哲学 杂谈:物理世界,虚拟世界
猜您喜欢 用深度链接优化用户增长策略 【R语言】文本挖掘、可视化 测试开发之路 - 可读性,可维护性,可扩展性 业务安全通用解决方案--WAF数据风控 “我要用科学干出一条生路”:科学人专访《火星救援》电影主创