专栏名称: 嘶吼专业版
为您带来每日最新最专业的互联网安全专业信息。
目录
相关文章推荐
FM1007福建交通广播  ·  抖音、快手、微信同日宣布:下架! ·  23 小时前  
FM1007福建交通广播  ·  抖音、快手、微信同日宣布:下架! ·  23 小时前  
四川生态环境  ·  和火爆全网的DeepSeek聊了聊四川生态环 ... ·  2 天前  
四川生态环境  ·  和火爆全网的DeepSeek聊了聊四川生态环 ... ·  2 天前  
网信湖北  ·  AI“洗稿”造谣,如何用魔法打败魔法 ·  3 天前  
网信湖北  ·  AI“洗稿”造谣,如何用魔法打败魔法 ·  3 天前  
Z Finance  ·  速递丨前OpenAI首席科学家Ilya创办的 ... ·  3 天前  
Z Finance  ·  速递丨前OpenAI首席科学家Ilya创办的 ... ·  3 天前  
北京药监  ·  市药监局高效服务 ... ·  4 天前  
北京药监  ·  市药监局高效服务 ... ·  4 天前  
51好读  ›  专栏  ›  嘶吼专业版

如何编写简单的linux内核模块

嘶吼专业版  · 公众号  · 互联网安全  · 2017-12-11 18:12

正文

获取Ring-0权限

尽管Linux系统为应用程序提供了强大而丰富的API,但是有的时候,这些还远远不够。当我们需要与硬件进行交互,或者需要访问系统中特权信息的时候,应用API就爱莫能助了,这时我们必须借助内核模块。

Linux内核模块是一段编译好的二进制代码,可以直接插入到Linux内核空间中,在ring 0级别运行,该级别不仅仅是x86-64处理器的最低层的运行级别,同时也是安全限制最少的一个级别。由于这里的代码完全不受限制,所以能够以令人难以置信的速度飞速运行,同时,它们还可访问系统中的任意内容。

来到内核世界

编写一个Linux内核模块并不是一件容易的事情。在修改内核的时候,您将面临数据丢失和系统损坏的风险。对于常规Linux应用程序来说,系统为它们提供了相应的安全网作为保护,但是内核代码却完全不是这样:内核代码一旦出现故障,将会锁定整个系统。

更糟糕的是,内核代码中出现的问题可能不会马上显现出来。如果内核模块加载后,系统立即被锁定的话,这还算是“最佳情况”。随着向模块添加的代码越来越多,我们将面临引入死循环和内存泄漏的风险。如果你不小心犯了这样的错误,随着机器的继续运行,这些代码占用的内存会持续增长。那么,最终会导致重要的内存结构,甚至缓冲区都被覆盖掉。

对于内核模块来说,传统应用程序的大部分开发范式都不适用。除了加载和卸载模块外,我们还需编写代码来响应系统事件,因为这里的代码并非以串行的模式运行。对于内核开发来说,我们要编写的是供应用程序使用的API,而非应用程序本身。

除此之外,我们在内核空间也无法访问各种标准库。虽然内核提供了一些常用的函数,比如printk(用作printf的替代品)和kmalloc(作用与malloc类似),但是大部分情况下,都需要我们亲自跟设备打交道。此外,在卸载模块时,我们必须亲自完成相关的清理工作,因为这里没有提供垃圾收集功能。

先决条件

在开始编写内核模块之前,我们需要确保已经准备好了得心应手的工具。最重要的是,你需要有一台Linux机器。虽然任何Linux发行版都可以满足我们的要求,但是在本文中,我使用的是Ubuntu 16.04 LTS,所以,如果你使用了其他版本的话,在安装的过程中,可能需要稍微调整一下相关的安装命令。

其次,你还需要一台单独的物理机器或虚拟机。虽然我更喜欢在虚拟机上完成这些工作,但是读者完全可以根据自己的喜好来作出决定。我不建议使用您的工作主机,因为一旦出错,就很可能会发生数据丢失的情况。同时,我们在编写内核模块的过程中,一般至少会锁定机器许多次,这个是不用怀疑的。内核出乱子的时候,最近更新的代码很可能还在向缓冲区中写入内容,所以,这就可能导致源文件损坏。如果在虚拟机上进行测试的话,就能够消除这种风险。

最后,您至少需要对C语言有一些基本的了解。由于C++运行时对于内核来说占用的空间太多了,因此,编写C代码对于内核开发来说是非常重要的。此外,为了与硬件进行交互,了解一些汇编语言方面的知识也是非常有帮助的。

安装开发环境

在Ubuntu上,我们需要运行下列命令:

apt-get install build-essential linux-headers-`uname -r`

上面的命令将安装必要的开发工具,以及这个示例内核模块所需的内核头文件。

对于下面的示例内核模块,我们假设读者是以普通用户身份运行的,而不是root用户,但是,要求读者拥有sudo权限。对于非root用户来说,sudo在加载内核模块时是必须的,尽管这样有些麻烦,但我们希望尽可能以root之外的身份来完成内核模块开发工作。

踏上征程

从现在开始,我们就要开始编写代码了。好了,让我们先准备好工作环境:

mkdir ~/src/lkm_example

cd ~/src/lkm_example

你可以启动自己最喜欢的编辑器(对于我来说,就是VIM),创建文件lkm_example.c,并输入以下代码:

#include

#include

#include

MODULE_LICENSE(“GPL”);

MODULE_AUTHOR(“Robert W. Oliver II”);

MODULE_DESCRIPTION(“A simple example Linux module.”);

MODULE_VERSION(“0.01”);

static int __init lkm_example_init(void) {

printk(KERN_INFO “Hello, World!\n”);

return 0;

}

static void __exit lkm_example_exit(void) {

printk(KERN_INFO “Goodbye, World!\n”);

}

module_init(lkm_example_init);

module_exit(lkm_example_exit);

现在,我们已经做好了一个最简单的内核模块,接下来,我会对一些重点内容加以详细说明:

·“includes”用于包含Linux内核开发所需的头文件。

· 根据模块的许可证的不同,MODULE_LICENSE可以设置为不同的值。要查看许可证的完整列表,请运行:

grep“MODULE_LICENSE”-B 27 / usr / src / linux-headers -`uname -r` / include / linux / module.h

· 我们将init(加载)和exit(卸载)函数定义为static类型,并让它返回一个int型数据。

· 注意,这里要使用printk函数,而不是printf函数。此外,printk与printf使用的参数也各不相同。例如,KERN_INFO(这是一个标志,用以声明相应的消息记录等级)在定义的时候并没有使用逗号。

· 在文件的最后部分,我们调用了module_init和module_exit函数,来告诉内核哪些是加载函数和卸载函数。这样的话,我们就能够给这些函数自由命名了。

当目前为止,我们仍然无法编译这个文件:我们还需要一个Makefile文件。有了它,这个简单的示例模块就算就绪了。请注意,make会严格区分空格和制表符,因此,在应该使用tab的地方千万不要使用空格。

obj-m += lkm_example.o

all:

make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:

make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

如果我们运行“make”,正常情况下应该成功编译我们的模块。最后得到的文件是“lkm_example.ko”。如果在此过程中出现了错误消息的话,请检查示例源文件中的引号是否正确,并确保没有意外粘贴为UTF-8字符。

现在,可以将我们的模块插入内核空间进行测试了。为此,我们可以运行如下所示的命令:

sudo insmod lkm_example.ko

如果一切顺利的话,屏幕上面是不会显示任何内容的。这是因为,printk函数不会将运行结果输出到控制台,相反,它会把运行结果输出到内核日志。为了查看内核模块的运行结果,我们需要运行下列命令:

sudo dmesg

正常情况下,这里应该看到带有时间戳前缀的“Hello,World!”行。这意味着我们的内核模块已经加载,并成功向内核日志输出了相关的字符串。我们还可以通过下面的命令,来检查该模块是否仍然处于加载状态:

lsmod | grep “lkm_example”

要删除该模块,请运行下列命令:

sudo rmmod lkm_example

如果您再次运行dmesg,则会在日志中看到字符串“Goodbye, World!”。同时,您也可以再次使用lsmod来确认它是否已被卸载。

正如你所看到的那样,这个测试工作流程有点繁琐而乏味,为了实现自动化,我们可以在Makefile文件末尾添加下列内容:

test:

sudo dmesg -C

sudo insmod lkm_example.ko

sudo rmmod lkm_example.ko

dmesg

然后,运行下列命令:

make test

这样的话,要想测试模块并查看内核日志的输出的话,就不必专门来运行相应的命令了。

现在,我们已经打造好了一个五脏俱全,但是没有什么用处的内核模块!

打造更有趣的内核模块

接下来,让我们通过具体的例子来进一步了解内核模块的开发。虽然内核模块可以完成各种任务,但最常见的用途,恐怕就是与应用程序进行交互了。

由于应用程序无法直接查看内核空间内存的内容,因此,它们必须借助API与其进行通信。虽然从技术上来说有多种方法可以实现这一点,但最常见的方法却是创建一个设备文件。

实际上,您很可能早就跟设备文件打过交道了。比如,涉及/dev/zero、/dev/null或类似文件的命令,实际上就是在跟名为“zero”和“null”的设备进行交互,以返回相应的内容。

在我们的例子中,我们将返回“Hello,World”。虽然对于应用程序来说,这一功能没有多大的用途,但它却为我们详细展示了通过设备文件响应应用程序的具体过程。

下面是完整的代码:

#include

#include

#include

#include

#include

MODULE_LICENSE(“GPL”);

MODULE_AUTHOR(“Robert W. Oliver II”);

MODULE_DESCRIPTION(“A simple example Linux module.”);

MODULE_VERSION(“0.01”);

#define DEVICE_NAME “lkm_example”

#define EXAMPLE_MSG “Hello, World!\n”

#define MSG_BUFFER_LEN 15

/* Prototypes for device functions */

static int device_open(struct inode *, struct file *);

static int device_release(struct inode *, struct file *);

static ssize_t device_read(struct file *, char *, size_t, loff_t *);

static ssize_t device_write(struct file *, const char *, size_t, loff_t *);

static int major_num;

static int device_open_count = 0;

static char msg_buffer[MSG_BUFFER_LEN];

static char *msg_ptr;

/* This structure points to all of the device functions */

static struct file_operations file_ops = {

.read = device_read,

.write = device_write,

.open = device_open,

.release = device_release

};

/* When a process reads from our device, this gets called. */

static ssize_t device_read(struct file *flip, char *buffer, size_t len, loff_t *offset) {

int bytes_read = 0;

/* If we’re at the end, loop back to the beginning */

if (*msg_ptr == 0) {

msg_ptr = msg_buffer;

}

/* Put data in the buffer */

while (len && *msg_ptr) {

/* Buffer is in user data, not kernel, so you can’t just reference

* with a pointer. The function put_user handles this for us */

put_user(*(msg_ptr++), buffer++);

len--;

bytes_read++;

}

return bytes_read;

}

/* Called when a process tries to write to our device */

static ssize_t device_write(struct file *flip, const char *buffer, size_t len, loff_t *offset) {

/* This is a read-only device */

printk(KERN_ALERT “This operation is not supported.\n”);

return -EINVAL;

}

/* Called when a process opens our device */

static int device_open(struct inode *inode, struct file *file) {

/* If device is open, return busy */

if (device_open_count) {

return -EBUSY;

}

device_open_count++;

try_module_get(THIS_MODULE);

return 0;

}

/* Called when a process closes our device */

static int device_release(struct inode *inode, struct file *file) {

/* Decrement the open counter and usage count. Without this, the module would not unload. */

device_open_count--;

module_put(THIS_MODULE);

return 0;

}

static int __init lkm_example_init(void) {

/* Fill buffer with our message */

strncpy(msg_buffer, EXAMPLE_MSG, MSG_BUFFER_LEN);

/* Set the msg_ptr to the buffer */

msg_ptr = msg_buffer;

/* Try to register character device */

major_num = register_chrdev(0, “lkm_example”, &file_ops);

if (major_num < 0) {







请到「今天看啥」查看全文


推荐文章
FM1007福建交通广播  ·  抖音、快手、微信同日宣布:下架!
23 小时前
FM1007福建交通广播  ·  抖音、快手、微信同日宣布:下架!
23 小时前