问题记录: 在内核模块中拦截报文并保存到文件

Release log:

2021-04-24 六: 完成初版

原文地址

背景介绍

最近在跟进一个网络相关的问题,需要查看经过 __netif_receive_skb 的报文是否有异常。如果打印所有的包,由于打印太多会影响性能,并且打印的内容也不会太详细。所以决定,把监听到的报文全部保存到 pcap 文件中,然后通过 wireshark 查看
这里对需要用到的知识做一个整理

知识点分析

  1. 怎么在内核模块中读写文件
  2. pcap 的文件格式是怎样的

怎么在内核中读写文件

一段简单的实例代码如下:

// main.c
#include <linux/module.h>	// 包含了对模块的结构定义以及模块的版本控制
#include <linux/kernel.h>	// 包含常用的内核函数
#include <linux/fs.h>

MODULE_LICENSE("Dual BSD/GPL");

static int __init skb2pcap_init(void)
{
	struct file *fp = filp_open("/tmp/kernel_rw", O_CREAT | O_WRONLY | O_TRUNC, 0644);
	const char *wstr = "write from kernel";
	int len = 0;
	mm_segment_t old_fs;

	if (IS_ERR(fp)) 
	{
		printk("[%s] open file failed\n", __func__);
		return -1;
	}

	old_fs = get_fs();
	/* 改变 kernel 对内存地址检查的处理方式, 如果此处被抢占是否会有风险 */
	set_fs(KERNEL_DS);
	len = vfs_write(fp, wstr, strlen(wstr), &fp->f_pos);
	set_fs(old_fs);
	filp_close(fp, NULL);

	printk("[%s] write len: %d\n", __func__, len);
    return 0;
}

static void __exit skb2pcap_exit(void)
{
	struct file *fp = filp_open("/tmp/kernel_rw", O_RDONLY, 0);
	int len = 0;
	char buf[128] = {0};
	mm_segment_t old_fs;

	if (IS_ERR(fp))
	{
		printk("[%s] open file failed\n", __func__);
		return;
	}

	old_fs = get_fs();
	set_fs(KERNEL_DS);
	len = vfs_read(fp, buf, sizeof(buf) - 1, &fp->f_pos);
	set_fs(old_fs);
	filp_close(fp, NULL);

	printk("[%s] get str[%d]: %s\n", __func__, len, buf);
}

module_init(skb2pcap_init);
module_exit(skb2pcap_exit);

# Makefile
# 如果已定义 KERNELRELEASE,则说明是从内核构造系统调用的,因此可利用其内建语句
ifneq ($(KERNELRELEASE), )
	obj-m := skb2pcap.o
	skb2pcap-objs := main.o
# 否则,是直接从命令行调用的,这时要用内核构造系统
else
	KERNELDIR ?= /lib/modules/$(shell uname -r)/build
	PWD := $(shell pwd)

default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
	# rm -f *.o *.cmd .*.cmd *.mod modules.order *.mod.c
endif

需要注意,vfs_read 和 vfs_write 原型中,buffer 指针是用 __user 修饰的,如果直接使用内核地址作为参数会返回失败。所以在使用前需要调用 set_fs 改变 kernel 对内存地址的检查方式

遇到问题

insmode 的时候提示错误

$ sudo insmod ./skb2pcap.ko 
insmod: ERROR: could not insert module ./skb2pcap.ko: Operation not permitted

使用 dmesg 查看,发现如下提示

Lockdown: insmod: unsigned module loading is restricted; see man kernel_lockdown.7

解决办法: 重启电脑,在 BIOS 中关闭 secure boot 功能,保存并重启。这个时候 insmod 就没问题了

但 insmode 时会发现,终端没有 printk 内容输出。其实输出内容可以在 dmesg 中看到

pcap 的文件格式是怎样的

pcap 文件的总体结构是: 文件头–数据包头1–数据包1–数据包头2–数据包2…

其中文件头的格式如下:

struct pcap_file_header {
	uint32_t magic;			// 为 0xa1b2c3d4 或者 0xd4c3b2a1,用来识别字节顺序,前者为大端模式,后者为小端模式
	uint16_t version_major;	// 主版本号,一般为 0x0200
	uint16_t version_minor;	// 次版本号,一般为 0x0400
	uint32_t thiszone;		// GMT 和本地时间的差值,单位秒。一般为零
	uint32_t sigfigs;		// 时间戳的精度,一般为全零
	uint32_t snaplen;		// 设置所抓获的数据包的最大长度,如果所有数据包都要抓获,将该值设置为65535
	uint32_t linktype;		// 链路类型
};
/* 链路类型对应关系如下
   0  					BSD loopback devices, except for later OpenBSD
   1  					Ethernet, and Linux loopback devices
   6  					802.5  Token Ring
   7  					ARCnet
   8  					SLIP
   9  					PPP
   10 					FDDI
   100					LLC/SNAP-encapsulated  ATM 
   101					"raw IP", with no link
   102					BSD/OS  SLIP
   103					BSD/OS  PPP
   104					Cisco  HDLC
   105					802.11
   108					later OpenBSD loopback devices (with the AF_value in network byte order)
   113					special  Linux  "cooked"  capture
   114					LocalTalk
   */

数据包头的格式如下:

struct pcap_pkt_header {
	uint32_t ts_s;			// 时间戳高位,精确到秒,从 GMT 开始记
	uint32_t ts_ms;			// 时间戳低位,精确到毫秒
	uint32_t caplen;		// 抓获到数据帧的长度
	uint32_t len;			// 离线数据长度,网路中实际数据帧的长度,一般不大于Caplen,多数情况下和Caplen值一样
};

一段简单的解析 pcap 报文的代码如下:

int main(int argc, char *argv[])
{
	int fd = -1;
	struct pcap_file_header fhdr = {0};
	struct pcap_pkt_header phdr = {0};
	uint8_t databuf[2048] = {0};
	int datalen = 0;

	if (2 != argc)
	{
		printf("usage: %s pcap_file_name\n", argv[0]);
		return 0;
	}

	if ((fd = open(argv[1], O_RDONLY)) < 0)
	{
		printf("open %s failed: %s\n", argv[1], strerror(errno));
		goto leave;
	}

	if (sizeof(fhdr) != read(fd, &fhdr, sizeof(fhdr)))
	{
		printf("read file failed: %s\n", strerror(errno));
		goto leave;
	}

	printf("file header info: magic(0x%08x), major_version(0x%04x), "
			"minor_version(0x%04x), thiszone(0x%08x), sigfigs(0x%08x), "
			"snaplen(0x%08x), linktype(0x%08x)\n", 
			fhdr.magic, fhdr.version_major, fhdr.version_minor, 
			fhdr.thiszone, fhdr.sigfigs, fhdr.snaplen, fhdr.linktype);

	while (sizeof(phdr) == read(fd, &phdr, sizeof(phdr)))
	{
		int i = 0;

		datalen = read(fd, databuf, phdr.caplen);
		if (phdr.caplen != datalen)
		{
			printf("read data len err(%d - %d): %s\n", phdr.caplen, datalen, strerror(errno));
			break;
		}

		printf("packet header info: ts_s(%d), ts_ms(%d), caplen(%d), len(%d):", 
				phdr.ts_s, phdr.ts_ms, phdr.caplen, phdr.len);

		for (i = 0; i < phdr.caplen; i ++)
		{
			if (0 == i % 16) printf("\n");
			else if (0 == i % 8) printf(" ");

			printf("%02x ", databuf[i]);
		}
		printf("\n");
	}

leave:
	if (fd >= 0) close(fd);

	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值