操作系统:ubuntu虚拟机
编程语言:LinuxC
(1).安装源更新
ubuntu系统默认的安装源是ubuntu官方源,从国外访问速率较慢,这儿先要将其替换成国内的安装源,找到:/etc/apt/sources.list文件,先将其备份,之后将上面的内容替换成以下内容:
debjammymainrestricteduniversemultiverse
debjammy-securitymainrestricteduniversemultiverse
debjammy-updatesmainrestricteduniversemultiverse
debjammy-proposedmainrestricteduniversemultiverse
debjammy-backportsmainrestricteduniversemultiverse
deb-srcjammymainrestricteduniversemultiverse
deb-srcjammy-securitymainrestricteduniversemultiverse
deb-srcjammy-updatesmainrestricteduniversemultiverse
deb-srcjammy-proposedmainrestricteduniversemultiverse
deb-srcjammy-backportsmainrestricteduniversemultiverse
debfocalmainrestricteduniversemultiverse
debfocal-updatesmainrestricteduniversemultiverse
debfocal-backportsmainrestricteduniversemultiverse
debfocal-securitymainrestricteduniversemultiverse
$sudoaptupdate
(2).环境配置
假定登入系统帐号为:suntiger,代码工程保存在目录:
/home/linux-kernel-security-module,在命令行下须要输入以下加壳命令:
#chown-Rsuntiger/home/linux-kernel-security-module
执行该命令后,在VScode中便可以正常创建代码文件。
在开发过程中,须要使用Linux具体版本的内核头文件包,通过以下命令进行安装:
#aptinstalllinux-headers-$(uname-r)
还须要安装一些基本的开发工具,比如:build-essential,它包含了编译内核模块所需的编译器和其它工具linux漏洞扫描,安装命令如下:
#aptinstallbuild-essential
#add-apt-repositoryppa:ubuntu-toolchain-r/test
#aptupdate

#aptinstallgcc-12g++-12
开发一个简单内核模块扩充
首先在自己的工程目录新建一个代码文件:main.c,之后写入以下代码:
#include
#include
staticint__initconstruct(void){
pr_info(“Firstkernelmodule:HelloWorld!n”);
return0;
staticvoid__exitdestruct(void){
pr_info(“Firstkernelmodulehasbeenremovedn”);
module_init(construct);
module_exit(destruct);
MODULE_LICENSE(“GPL”);
这段代码是一个简单的Linux内核模块开发实例,以下部份将对这段代码的涵义进行剖析。
首先是头文件:
#include
#include
这种头文件包含了内核模块开发所需的函数和宏定义,比如:
下边来看这段代码:
staticint__initconstruct(void){
pr_info(“Firstkernelmodule:HelloWorld!n”);
return0;
这段代码是模块的初始化函数,它在模块加载时被调用。函数名后面的__init是一个宏,用于将函数标记为初始化代码。在模块加载时,内核会调用这个函数。
下边看另一段代码:
staticvoid__exitdestruct(void){
pr_info(“Firstkernelmodulehasbeenremovedn”);
这是模块的清除函数linux内核开发头文件,它在模块卸载时被调用。函数名后面的__exit是一个宏,用于将函数标记为退出代码,在模块卸载时,内核会调用这个函数。
看最后的三行代码:
module_init(construct);
module_exit(destruct);
MODULE_LICENSE(“GPL”);
前两行宏代码用于注册初始化和清除函数,其中:
最后一句宏定义了模块的许可证为GPL(GNUGeneralPublicLicense)。这表明该模块遵守GPL许可证。使用GPL许可证申明是强烈推荐的,非常是当你要将模块发布为开源代码时。内核模块假如没有指定许可证,内核将觉得它是非GPL的,而且可能限制个别符号的使用。
下边编撰一个Makefile来尝试编译一下代码,内容如下:
obj-m+=main.o
all:
make-C/lib/modules/$(shelluname-r)/buildM=$(PWD)modules
clean:
make-C/lib/modules/$(shelluname-r)/buildM=$(PWD)clean
注意:Makefile的内容对格式有严格要求,make后面的空格必须是一个table空格,否则会引起编译错误。
第一句:obj-m+=main.o定义了一个目标模块main.o。obj-m是一个内核建立系统变量红旗linux6.0教程,用于列举要建立的模块对象文件。通过将main.o添加到obj-m中,这是为了告诉内核建立系统,我们要编译main.c并生成main.o内核模块。
第二句:
all:
make-C/lib/modules/$(shelluname-r)/buildM=$(PWD)modules
下边切换到当前的工作目录,在命令行中直接输入Make命令开始编译,编译过程如下:

假如编译成功,在当前的工作目录会看见一个名为main.ko的内核模块,如图:

下边,使用以下命令将编撰的模块加载到内核中,命令如下:
#insmodmain.ko
接着输入lsmod命令查看已加载的内核模块,如图:

可以看见自己编撰的内核模块已加载成功。
接着输入命令:dmesg|grepmain-A1来查看模块的内核日志,如图:

可以看见加载的内核扩充模块日志成功复印,说明已成功加载运行。
接出来,尝试输入命令:rmmodmain来卸载内核模块,并使用命令dmesg|grepmain-A2来查看内核日志,如图:

从日志内容中可以发觉卸载模块信息被复印,说明内核扩充卸载成功,使用命令lsmod再度查看内核模块,如图:

main内核扩充已完全卸载。
内核扩充中使用日志级别
在内核扩充中,也可以复印不同日志级别的信息,看下边的代码:
#include
#include
staticint__initconstruct(void){
printk(KERN_INFO”INFOLevelmessagen”);
pr_info(“AnotherINFOLevelmessagen”);
printk(KERN_WARNING”WARNINGLevelmessagen”);
pr_warn(“AnotherWARNINGLevelmessagen”);
printk(KERN_ERR”ERRORLevelmessagen”);
pr_err(“AnotherERRORLevelmessagen”);
printk(KERN_CONT”This”);
pr_cont(“message”);
printk(KERN_CONT”is”);
pr_cont(“single”);
printk(KERN_CONT”linen”);
return0;
staticvoid__exitdestruct(void){
printk(KERN_INFO”Themodulehasbeenremovedn”);
module_init(construct);
module_exit(destruct);
MODULE_LICENSE(“GPL”);
在前面的代码中,主要是用了printk()和pr_xxx系列函数来复印不同日志级别的信息,二者涵义差不多。
编译该内核扩充并加载后,可以使用以下命令查看错误级别的日志:
#dmesg--levelerr|grepmessage
返回信息如图:
开发网路数据包拦截内核扩充
有了前面的基础,下边开发一个网路数据包拦截的内核扩充,代码如下:
#include//includedforallkernelmodules
#include//includedforKERN_INFO
#include//includedfor__initand__exitmacros
#include
#include
#include
#include
MODULE_LICENSE(“GPL”);
MODULE_DESCRIPTION(“ASimpleHelloPacketModule”);
enum{NF_IP_PRE_ROUTING,
NF_IP_LOCAL_IN,
NF_IP_FORWARDlinux内核开发头文件,
NF_IP_LOCAL_OUT,
NF_IP_POST_ROUTING,
NF_IP_NUMHOOKS};
staticstructnf_hook_opsin_nfho;//netfilterhookoptionstruct
staticstructnf_hook_opsout_nfho;//netfilterhookoptionstruct
staticvoiddump_addr(unsignedchar*iphdr)
inti;
for(i=0;iname);
unsignedchar*iphdr=skb_network_header(skb);
if(iphdr){
dump_addr(iphdr);
returnNF_ACCEPT;
//returnNF_DROP;//会造成上不了网
staticint__initinit_func(void)
//NF_IP_PRE_ROUTINGhook
in_nfho.hook=my_hook;
in_nfho.hooknum=NF_IP_LOCAL_IN;
in_nfho.pf=PF_INET;
in_nfho.priority=NF_IP_PRI_FIRST;
nf_register_net_hook(&init_net,&in_nfho);
//NF_IP_LOCAL_OUThook
out_nfho.hook=my_hook;
out_nfho.hooknum=NF_IP_LOCAL_OUT;
out_nfho.pf=PF_INET;
out_nfho.priority=NF_IP_PRI_FIRST;
nf_register_net_hook(&init_net,&out_nfho);
return0;
staticvoid__exitexit_func(void)
nf_unregister_net_hook(&init_net,&in_nfho);
nf_unregister_net_hook(&init_net,&out_nfho);
printk(KERN_INFO”CleaningupHello_Packetmodule.n”);
module_init(init_func);
module_exit(exit_func);
在前面的代码中,注册并设置了两个Netfilter钩子:
最终通过dump_addr函数复印了数据包中的源IP地址和目的IP地址。
将代码根据前面的方式进行编译并加载,模块加载完成后,输入命令:
dmesg|tail
可以看见复印下来的源IP地址和目的IP地址,如图:

这儿192.168.30.57是源IP地址,也就是我本地工作笔记本地址;192.168.201.204就是服务器地址。
