19

lkm入门&netlink通信示例 | Spoock

 4 years ago
source link: https://blog.spoock.com/2019/11/25/lkm/?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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的可执行文件。

  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

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!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK