lkm入门&netlink通信示例
发表于
2019-11-25
在研究过程中,发现LKM始终绕不过去,于是打算围绕LKM,系统调用,系统调用表写几篇相关的文章。
KM是loadable kernel module的简称, 即可加载的内核模块,是一段运行在内核空间的代码,可以动态加载,无需重新实现整个内核.
首先这个内核不同与微内核的模块,微内核的模块是一个个的daemon进程,工作于用户空间.Linux的模块只是一个内核的目标代码,内核通过执行运行时的连接,来把它整合到kernel中,所以说Linux的模块机制并没有改变Linux内核的单内核的本质.其模块也是工作于内核模式,享有内河的所有的特权.
引入LKM的好处有3点:
- 模块化编程的需要,降低和维护成本.
- 增强系统的灵活性,使得修改一些内核功能而不必重新编译内核或重启系统
- 降低内核编程的复杂性,是入门门槛降低.
通过LKM可以在运行时动态地更改Linux. 可动态更改是指可以将新的功能家在到内核,从内核除去某个功能,甚至添加使用其他LKM.LKM的优点是可以最小化内核的内存占用.只加载需要的元素.
参考:Linux 2.6.x 内核模块入门(LKM)
我们可以通过Makefile编译我们自己写好的内核模块.Makefile编译的到的内核模块是以ko结尾,可以使用以下几个命令对内核模块进行操作.
- insmod 安装内核模块
- rmmod 卸载内核模块
- lsmod 查看内核模块
- modinfo 用于查询模块的相关信息 ,比如作者,版权
- modprobe 用于智能地向内核中加载模块或者从内核中移除模块
我们可以通过module_parame(name,type,perm)函数在加载内核模块时向其传递参数,通过不同的参数选项以期达到不同的效果. name是变量名,type是变量类型,perm是权限.
内核模块和应用程序的区别
CPU执行模式
在Intel x86架构中,有四种模式,也叫ring0-ring3,模式之间的权限不同,这里的权限指的是对硬件设备的操作,如读写内存,读写硬盘等.
Linux使用其中两种模式,即内核模式(ring 0)/特权模式(supervisor mode),用户模式(ring 3)/非特权模式(user mode).应用程序跑的代码,包括所调用的C标准库都是跑在用户模式下.而内核模式下跑的代码,都是跑在内核模式中.用户模式想要进入到内核模式,入口之一便是系统函数调用.
用户态和内核态之间的交互
用户态和内核态进行交互的方式之一,是上面我们提到过的系统函数调用.什么是系统调用?你可以简单认为,libc调用下层函数,就是系统调用函数,如libc中open()的实现,最终需要调用系统调用函数__NR_open()进入内核,在内核态访问硬盘,打开文件.
参考:内核模块和应用程序的区别
之后会有文章说明如何使用LKM Hook内核函数。
LKM入门编写
简单的lkm
以下展示的一个最为简单的LKM.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <linux/module.h> // 加载内核模块到内核使用的核心头文件 #include <linux/init.h> // 用于标记函数的宏,如__init,__exit #include <linux/kernel.h> // 包含内核使用的类型,宏和函数
MODULE_LICENSE("GPL"); // 许可证类型 MODULE_AUTHOR("SPOOCK") // 作者 当使用modinfo命令时可见 MODULE_DESCRIPTION("A SIMPLE LKM") // 描述信息 使用modinfo可见 MODULE_VERSION("0.1"); // 模块版本
static char *name = "world"; // 模块参数,默认值是world module_param(name, charp, S_IRUGO); // 参数定义,charp表示字符指针(char ptr) MODULE_PARM_DESC(name, "The name to display in /var/log/kern.log"); ///< 参数描述 int my_module_init( void ) { printk(KERN_INFO "my_module_init called. Module is now loaded.The parameter name is %s\n",name); return 0; } /* Cleanup function called on module exit */ void my_module_cleanup( void ) { printk(KERN_INFO "my_module_cleanup called. Module is now unloaded.\n"); return; } /* Declare entry and exit functions */ module_init( my_module_init ); module_exit( my_module_cleanup );
|
- 编写的LKM,我们需要声明为GPL协议.因为内核是基于GPL发布的,许可的选择会影响内核处理模块的方式.如果对非GPL代码选择专有许可,内核将会把模块标记为污染的(tainted),并且显示告警.除了GPL协议之外,我们也可以选择GPLv2,BSD/GPL,MIT/GPL,MPL/GPL.
- 模块参数被声明为static char * 类型,并且初始化为hello.在内核模块中应该避免使用全局变量,因为全局变量是被整个内核共享的,需要使用static关键字限制变量在模块中的作用域.如果必须使用全局变量,需要在变量名上增加前缀保证在模块中是唯一的.
- module_param 作用在第二节中已经说明.
- my_module_init()函数是在加载这个模块时被调用,一般是用来进行一些初始化的工作(在本例中仅仅只是简单地进行打印).my_module_cleanup()函数是在卸载这个模块时被调用,一般是用来释放内存并清除这个模块的踪迹.
- printk()是内核中的printf()函数.可以在内核模块代码的任何地方调用该函数.需要注意的是当调用printk()函数时,必须提供日志级别.日志级别在linux/kern_levels.h头文件中定义.它的值为 KERN_EMERG、KERN_ALERT、KERN_CRIT、KERN_ERR、KERN_WARNING、KERN_NOTICE、KERN_INFO、KERN_DEBUG 和 KERN_DEFAULT 之一。该头文件通过 linux/printk.h 文件被包含在 linux/kernel.h 头文件中
- 最后使用module_init和module_exit宏生命了入口函数和出口函数,这样我们就可以按照自己的意愿来对这个模块的init和exit操作的进行关联.
Makefile编写
lkm编写完毕之后,接下来就是编写Makefile文件编译的到ko.Makefile的写法是:
1 2 3 4 5 6
| obj-m := simple-lkm.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd)
default: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules
|
直接在当前目录运行make命令,运行结果如下;
1 2 3 4 5 6 7 8 9 10
| # make make -C /lib/modules/5.0.0-29-generic/build SUBDIRS=/home/ubuntu/Desktop/lkm modules make[1]: Entering directory '/usr/src/linux-headers-5.0.0-29-generic' Makefile:223: ================= WARNING ================ Makefile:224: 'SUBDIRS' will be removed after Linux 5.3 Makefile:225: Please use 'M=' or 'KBUILD_EXTMOD' instead Makefile:226: ========================================== Building modules, stage 2. MODPOST 1 modules make[1]: Leaving directory '/usr/src/linux-headers-5.0.0-29-generic'
|
出现上面的结果就表示成功运行,在当前目前下就会生成一个simple-lkm.ko
的文件.
查看运行结果
modinfo查看信息
1 2 3 4 5 6 7 8 9 10 11 12
| # modinfo simple-lkm.ko filename: /home/ubuntu/Desktop/lkm/simple-lkm.ko version: 0.1 description: A SIMPLE LKM author: SPOOCK license: GPL srcversion: 7C07352526A339C7BD02154 depends: retpoline: Y name: simple_lkm vermagic: 5.0.0-29-generic SMP mod_unload parm: name:The name to display in /var/log/kern.log (charp)
|
insmod 加载模块
1 2 3 4
| # insmod simple-lkm.ko # lsmod | grep simple simple_lkm 16384 0 # rmmod simple_lkm
|
通过insmod成功加载了模块,使用rmmod成功卸载了模块
查看模块运行信息
内核的输出进到了内核回环缓冲区中,而不是打印到 stdout 上,这是因为 stdout 是进程特有的环境。要查看内核回环缓冲区中的消息,可以使用 dmesg 工具(或者通过 /proc 本身使用 cat /proc/kmsg 命令)。
1 2 3
| # dmesg | tail -2 [ 1288.115412] my_module_init called. Module is now loaded.The parameter name is world [ 1443.850566] my_module_cleanup called. Module is now unloaded.
|
内核的信息也成功在dmesg中显示出来了
参考:编写 Linux 内核模块——第一部分:前言
用户态通过netlink与LKM通信
很多时候我们需要编写LKM模块从内核获取信息,用户态接受LKM捕获的信息。此时,我们可以通过neltink来完成通信获取数据。关于netlink的内容,之后会写文章对其进行说明。
由于在用户态的程序需要与LKM模块进行通信,所以存在两个程序。分别是LKM以及用户态程序。下面的示例程序来自于 How to use netlink socket to communicate with a kernel module?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| #include <linux/module.h> #include <net/sock.h> #include <linux/netlink.h> #include <linux/skbuff.h> #define NETLINK_USER 31
struct sock *nl_sk = NULL;
static void hello_nl_recv_msg(struct sk_buff *skb) {
struct nlmsghdr *nlh; int pid; struct sk_buff *skb_out; int msg_size; char *msg = "Hello from kernel"; int res;
printk(KERN_INFO "Entering: %s\n", __FUNCTION__);
msg_size = strlen(msg);
nlh = (struct nlmsghdr *)skb->data; printk(KERN_INFO "Netlink received msg payload:%s\n", (char *)nlmsg_data(nlh)); pid = nlh->nlmsg_pid; /*pid of sending process */
/* 创建sk_buff 空间 */ skb_out = nlmsg_new(msg_size, 0); if (!skb_out) { printk(KERN_ERR "Failed to allocate new skb\n"); return; }
/* 设置netlink消息头部 */ nlh = nlmsg_put(skb_out, 0, 0, NLMSG_DONE, msg_size, 0); NETLINK_CB(skb_out).dst_group = 0; /* not in mcast group */
/* 拷贝数据发送 */ strncpy(nlmsg_data(nlh), msg, msg_size);
res = nlmsg_unicast(nl_sk, skb_out, pid); if (res < 0) printk(KERN_INFO "Error while sending bak to user\n"); }
static int __init hello_init(void) {
printk("Entering: %s\n", __FUNCTION__); //nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, 0, hello_nl_recv_msg, NULL, THIS_MODULE); struct netlink_kernel_cfg cfg = { .input = hello_nl_recv_msg, };
nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, &cfg); if (!nl_sk) { printk(KERN_ALERT "Error creating socket.\n"); return -10; }
return 0; }
static void __exit hello_exit(void) {
printk(KERN_INFO "exiting hello module\n"); netlink_kernel_release(nl_sk); }
module_init(hello_init); module_exit(hello_exit);
MODULE_LICENSE("GPL");
|
Makefile文件
1 2 3 4 5 6
| obj-m := netlinklkm.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd)
default: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules
|
用户态的程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| #include <linux/netlink.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <unistd.h>
#define NETLINK_USER 31
#define MAX_PAYLOAD 1024 /* maximum payload size*/ struct sockaddr_nl src_addr, dest_addr; struct nlmsghdr *nlh = NULL; struct iovec iov; int sock_fd; struct msghdr msg;
int main() {
/* 创建NETLINK socket */ sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_USER); if (sock_fd < 0) return -1;
memset(&src_addr, 0, sizeof(src_addr)); src_addr.nl_family = AF_NETLINK; src_addr.nl_pid = getpid(); /* self pid */
bind(sock_fd, (struct sockaddr *) &src_addr, sizeof(src_addr));
memset(&dest_addr, 0, sizeof(dest_addr)); dest_addr.nl_family = AF_NETLINK; dest_addr.nl_pid = 0; /* For Linux Kernel */ dest_addr.nl_groups = 0; /* unicast */
nlh = (struct nlmsghdr *) malloc(NLMSG_SPACE(MAX_PAYLOAD)); memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD)); nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD); nlh->nlmsg_pid = getpid(); nlh->nlmsg_flags = 0;
strcpy(NLMSG_DATA(nlh), "Hello");
iov.iov_base = (void *) nlh; iov.iov_len = nlh->nlmsg_len; msg.msg_name = (void *) &dest_addr; msg.msg_namelen = sizeof(dest_addr); msg.msg_iov = &iov; msg.msg_iovlen = 1;
printf("Sending message to kernel\n"); sendmsg(sock_fd, &msg, 0); printf("Waiting for message from kernel\n");
/* Read message from kernel */ recvmsg(sock_fd, &msg, 0); printf("Received message payload: %s\n", NLMSG_DATA(nlh)); close(sock_fd); }
|
编译客户端程序,得到netlinkclient的可执行文件。
加载lkm
1 2 3
| # insmod netlinklkm.ko # lsmod | grep netlink netlinklkm 16384 0
|
运行客户端程序
1 2 3 4 5
| $ gcc netlinkclient.c -o netlinkclient $ ./netlinkclient Sending message to kernel Waiting for message from kernel Received message payload: Hello from kernel
|
netlink_kernel_create
netlink_kernel_create内核函数用于创建内核socket与用户态通信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| static inline struct sock * netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg) /* net: net指向所在的网络命名空间, 一般默认传入的是&init_net(不需要定义); 定义在net_namespace.c(extern struct net init_net); unit:netlink协议类型 cfg: cfg存放的是netlink内核配置参数(如下) */
/* optional Netlink kernel configuration parameters */struct netlink_kernel_cfg { unsigned int groups; unsigned int flags; void (*input)(struct sk_buff *skb); /* input 回调函数 */ struct mutex *cb_mutex; void (*bind)(int group); bool (*compare)(struct net *net, struct sock *sk); };
|
在本例中,我们仅仅只是设置了input参数,即回调函数.
netlink_unicast() && netlink_broadcast()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| /* 发送单播消息 */ extern int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock); /* ssk: netlink socket skb: skb buff 指针 portid: 通信的端口号 nonblock:表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函数在没有接收缓存可利用定时睡眠 */
/* 发送多播消息 */ extern int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, __u32 portid,__u32 group, gfp_t allocation); /* ssk: 同上(对应netlink_kernel_create 返回值)、 skb: 内核skb buff portid: 端口id group: 是所有目标多播组对应掩码的"OR"操作的合值。 allocation: 指定内核内存分配方式,通常GFP_ATOMIC用于中断上下文,而GFP_KERNEL用于其他场合。这个参数的存在是因为该API可能需要分配一个或多个缓冲区来对多播消息进行clone */
|
单播和多播的区别在于:
- 单播模式一般来说需要用户空间向内核发送消息后,内核才可以向用户空间发送
- 一般用于内核主动向用户空间报告一些内核状态,例如我们在用户空间看到的USB的热插拔事件的通告就是这样的应用
netlink type
netlink存在很多种类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #define NETLINK_ROUTE 0 /* Routing/device hook */ #define NETLINK_UNUSED 1 /* Unused number */ #define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */ #define NETLINK_FIREWALL 3 /* Unused number, formerly ip_queue */ #define NETLINK_SOCK_DIAG 4 /* socket monitoring */ #define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */ #define NETLINK_XFRM 6 /* ipsec */ #define NETLINK_SELINUX 7 /* SELinux event notifications */ #define NETLINK_ISCSI 8 /* Open-iSCSI */ #define NETLINK_AUDIT 9 /* auditing */ #define NETLINK_FIB_LOOKUP 10 #define NETLINK_CONNECTOR 11 #define NETLINK_NETFILTER 12 /* netfilter subsystem */ #define NETLINK_IP6_FW 13 #define NETLINK_DNRTMSG 14 /* DECnet routing messages */ #define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */ #define NETLINK_GENERIC 16 /* leave room for NETLINK_DM (DM Events) */ #define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */ #define NETLINK_ECRYPTFS 19 #define NETLINK_RDMA 20 #define NETLINK_CRYPTO 21 /* Crypto layer */
#define NETLINK_INET_DIAG NETLINK_SOCK_DIAG
#define MAX_LINKS 32
|
在本例中的示例程序使用的NETLINK_USER,即自定义的消息类型。同时本例中采用的是netlink_unicast()单播的发送方式。
更多的netlink通信的例子,参考:https://www.jianshu.com/p/073bcd9c3b08
本篇文章只是给出了一个简单的lkm入门.但是通过lkm,我们能够深入到内核层进行更多的操作,这无论是对于我们防御还是入侵都是一个新的挑战。
Linux Rootkit系列一:LKM的基础编写及隐藏
rootkit-sample-code
lkm-rootkit
Linux内核模块基础
编写 Linux 内核模块——第一部分:前言
使用 /proc 文件系统来访问 Linux 内核的内容
Linux Rootkit 实验
Kernel Module实战指南(一):Hello World!