lkm入门&netlink通信示例

说明

在研究过程中,发现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的可执行文件。

  1. 加载lkm

    1
    2
    3
    # insmod netlinklkm.ko
    # lsmod | grep netlink
    netlinklkm 16384 0
  2. 运行客户端程序

    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内核函数用于创建内核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存在很多种类型

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!