1. 前言
好久之前就看到了吾爱论坛上一师傅的帖子,结果因为各种事情,兜兜转转直到现在才勉勉强强把复现给搞定。既然复现了,那就写篇帖子来记录下吧。当然,也感谢windy_ll师傅热心帮助。
2. 环境
因为不太熟悉内核,所以这里选择采用和师傅差不多的版本配置,问题应该不大。
1 | 内核版本:5.4.0-109 |
1 | uname -a |
3. kernel module
大多数Linux内核模块是C写的,虽然Makefile可以多文件编译,但是出于学习的目的,所以本文不会出现多文件编译,初学的话还是推荐将模块使用单一文件保存。
3.1 模块构造
首先,我们来看一下常见的头文件以及预定义的宏
1 |
|
1 | include:涵盖了Linux内核开发所需要的头文件 |
定义完了基础的头文件以及宏,那么接下来就是我们的代码主体部分了。这里我采用了网上流传最多,也是大家最喜欢的代码”hello world”
1 | static int __init example_init(void) |
大家可以看到,我们代码的主题部分一共分为了三块。分别是我们的初始化(加载)、退出(卸载)、调用三部分。
首先是我们的__init
和__exit
,这两个是我们初始化以及退出的属性标志,说白了就是让我们的代码在编译过程中分别加载到它该去的地方。代码就放到.init.text
节,数据就放入.init.data
节,至于具体流程就不写了,感兴趣的可以去了解下kernel的编译加载和常规C的编译加载有何不同。
看完了init
和exit
接下来就是我们的两个函数声明了。
在代码的最后,我们调用module_init
和module_exit
来告诉内核哪个是加载函数,哪个是卸载函数。这样我们就可以按照自己的习惯来对函数自由命名了。
完整代码如下所示:
1 |
|
3.2 Makefile
构造完了我们的kernel module,那么接下来我们要解决的就是编译问题了。如何编译也是一门学问,这里编译kernel的话,我们需要用到Makefile文件。
Makefile的代码其实不长,这里我先直接全部贴出来:
1 | obj-m += mode_1.o |
值得注意的是,make对文件的空格
以及tab
键要求异常苛刻,一个字符的缩进错误都不允许,所以在编写Makefile的时候需要格外注意。
3.2.1 obj-m
我们先来看下我们的开头
1 | obj-m += mode_1.o |
obj-m
表示把文件mode_1.o作为”模块”进行编译,不会编译到内核,但是会生成一个独立的”mode_1.ko”文件。与之相对的则是我们的obj-y
,编译到内核,使用方法不在赘述。
当然,如果你有选择困难症的话,也可以让计算机自己来选择
1 | obj-$(CONFIG_FOO) |
$(CONFIG_FOO)可以是y(编译到内核)或m(编译到模块),如果两者皆不是的话,那么文件将不会被编译链接。
当然,毕竟我菜啊,所以这里咱直接选择最大众化也是比较经典的编译到模块中obj-m
。
3.2.2 all与clean
all、clean
这一类是Makefile中的伪目标,伪目标并不是一个真正的编译目标,它代表着一系列你想要执行的命令集合,通常一个Makefile会对应多个操作,例如编译、清除编译结果、安装,就可以使用这些伪目标来进行标记,在执行时则可以键入:
1 | make clean |
等指令来完成相应的指令操作,当make后不带参数时,默认执行第一个伪目标的操作。
3.2.3 make command
1 | make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules |
上面是一个标准的make指令,模板如下
1 | make -C $KDIR M=$PWD [target] |
-C:此选项指定内核源码的位置,make在编译时将会进入内核源码目录,执行编译,编译完成时返回。
$KDIR:/lib/modules/$(shell uname -r)/build
,指定内核源码位置。
直接在目标上编译时,内核头文件默认存放在
/lib/modules/$(shell uname -r)/build
中,这个build/
目录是一个软链接,链接到源码头文件的安装位置。而内核真正的源码库则直接引用正在运行的内核镜像。当跨平台编译时,就需要指定相应的内核源码目录,而不是系统中的源码目录。
M=$PWD:需要编译的模块源文件地址。
[target]:modules
,实际上,这个是个可选项。默认行为是将源文件编译并生成内核模块,即module(s),但是它还支持以下选项:
- modules_install:安装这个外部模块,默认安装地址是
/lib/modules/$(uname -r)/extra/
,同时可以由内建变量INSTALL_MOD_PATH指定安装目录。 - clean:卸载源文件目录下编译过程生成的文件,在上文的Makefile最后一行可以看到。
- help:帮助信息。
3.2.4 more options
hello world是我们学习编程时的第一个项目,它是最简单的也是最经典的,但是我们在实际开发环境中往往面临着更为复杂的情况,为了更好的应对这些情况我们再来看下更多的Makefile选项。
首先,当一个.o目标文件的生成依赖多个源文件时,显然mark的自动推导规则就力不从心了(它只能根据同名推导,比如编译example.o,只会去查找example.c),因此我们可以这样指定:
1 | obj-m += hello.o |
hello.o
目标文件依赖于a.o、b.o、hello_world.o
,那么这里的a.o
和b.o
如果没有指定源文件,根据推导规则就是依赖源文件a.c、b.c、hello_world.c
。除了hello-y
,同时也可以用hello-objs
,实现效果相同。
3.2.5 make和Makefile
为了让大家更好的学习与了解(水文章),这里我们来简单的看下make与Makefile的区别。
首先是make,make是Linux下的一个程序软件,而Makefile则相当于针对make程序的配置文件。当我们执行make命令时,make将会在当前目录寻找Makefile文件,然后根据Makefile的配置对源文件进行编译。
Linux内核源代码的编译也是使用make和Makefile,但是它在普通的C程序编译的基础上对配置和编译选项进行了扩展,这就是kbuild系统,专门针对Linux的内核编译,使得Linux内核的编译更加简洁高效。
3.3 函数与命令
3.3.1 printk函数
- printf:glibc实现的打印函数,工作于用户空间
- printk:内核模块无法使用glibc库函数,因此内核自身实现了一个类printf函数,但是需要指定打印等级
- define KERN_EMERG “<0>” 通常是系统崩溃前的信息
- #define KERN_ALERT “<1>” 需要立即处理的消息
- #define KERN_CRIT “<2>” 严重情况
- #define KERN_ERR “<3>” 错误情况
- #define KERN_WARNING “<4>” 有问题的情况
- #define KERN_NOTICE “<5>” 注意信息
- #define KERN_INFO “<6>” 普通消息
- #define KERN_DEBUG “<7>” 调试信息
查看当前系统printk打印等级:cat /proc/sys/kernel/printk
,从左到右依次对应当前控制台日志级别、默认消息日志级别、最小的控制台级别、默认控制台级别
3.3.2 dmesg
Linux dmesg(display message or display driver),用来在Unix-like系统中显示内核的相关信息。
实际上,dmesg命令是从内核环形缓冲区中获取数据的。当我们在Linux上排除故障时,dmesg命令会十分方便,它能很好地帮我们鉴别硬件相关的error和warning。除此之外,dmesg命令还能打印出守护进程相关的信息,以帮助我们debug。
上图是dmesg的help手册,用到时可以随时查看。这里我们只讲两个我比较喜欢的命令参数。
1 | dmesg -C // 清理dmesg缓存 |
正如图中所示,我先清理了dmesg的缓存内容,再次查看时就没有多余的消息了。接着使用-w
参数,为了效果,我分别进行了module的加载与卸载,可以看到dmesg的message一直在同步消息。
3.3.3 lsmod
lsmod列出当前内核中的所有模块,格式化显示在终端,其原理就是将/proc/modules
中的信息调整一下格式输出。lsmod输出列表有一列Used by,它表明此模块正在被其他模块使用,显示了模块之间的依赖关系。
3.3.4 insmod
如果要将一个模块加载到内核中,insmod是最简单的方法,insmod+模块完整路径就能达到目的,前提是你的模块不依赖其他模块,还需要注意需要sudo权限。如果不确定是否使用到其他模块的符号,也可以尝试modprobe
,详细用法可以参考modprobe -h
。
通过insmod
命令加载mode_1.ko
模块,加载该模块的时候会自动执行module_init()
函数,该函数打印了hello world!\n
。再次查看已载入系统的内核模块时,我们就可以在列表中发现mode_1.ko
。
3.3.5 rmmod
rmmod工具仅仅是将内核中运行的模块删除,只需要传给它路径就能实现。
rmmod命令卸载某个内存模块时,内存模块会自动执行__exit()
函数,进行清理操作。我们的__exit()
函数中打印了一行Good, world!\n
内容,同样可以使用dmesg查看。
3.3.6 modinfo
modinfo用来查看内核模块的相关信息,这里我们随机选中一个模块来看看。
在图中我们可以看到,bridge
模块存在的位置、遵循的协议等等信息。
4. kernel rootkit
因本人才疏学浅,rootkit部分的代码不会写,所以我就直接copy师傅的了。关于代码的解释感兴趣的可以去参考师傅的帖子。接下来我们直接上代码测试。
4.1 rootkit.c
1 |
|
因为师傅只写了socket的客户端,因此需要注意下这里的IP和Port,IP需要是本机IP,Port可随意填写。但需要注意的是,这两个数据一定要和后面的python脚本中的数据一样。
4.2 Makefile
1 | obj-m += rootkit.o |
4.3 testsock.py
这里是一个测试rootkit.c的用例,代码如下(同样来源于windy_ll
师傅)
1 | import socket |
注意这里的IP和Port,需要和上面的相同。
4.4 demo测试
- 首先我们先开启一个终端,启动testsock.py,进行监听。
- 接着我们将我们编译好的rootkit内核模块导入。
编译完成,insmod导入,同时dmesg开启监听
此时在另外一端成功连接到socket,输入?
查看帮助
4.4.1 命令执行
执行whoami
命令,可以观察到输出以及dmesg中的打印信息
4.4.2 隐藏内核模块
在终端执行lsmod查看内核模块,然后调用隐藏功能,查看效果
可以观察到已经成功隐藏了我们的内核模块。
4.4.3 显示内核模块
在开启该功能后,我们又成功查看到了该内核模块
4.4.4 隐藏文件
接下来是隐藏文件,此时我们还可以看到aaa.txt这个文件
接下来开启隐藏文件功能
大家可以看到,在我们选择了隐藏aaa.txt后,就无法在查看到该文件了。
4.4.5 关闭该内核模块
测试完了demo后就是关闭了,直接执行exit
即可
参考链接
1 | https://www.cnblogs.com/zackary/p/13951642.html |
发布时间: 2022-09-28
最后更新: 2023-07-21
本文标题: Kernel_module学习
本文链接: https://foxcookie.github.io/2022/09/28/Kernel_module学习/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!