说明
在研究过程中,发现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
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
6obj-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 | # modinfo simple-lkm.ko |
insmod 加载模块
1 | # insmod simple-lkm.ko |
通过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
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
6obj-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
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
15static 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
/* leave room for NETLINK_DM (DM Events) */
在本例中的示例程序使用的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!