百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

动手写一个基于Linux内核的网络数据包拦截扩展

gudong366 2025-03-26 11:09 8 浏览

操作系统: ubuntu虚拟机

编程语言: Linux C

(1). 安装源更新

ubuntu系统默认的安装源是ubuntu官方源,从国内访问速度较慢, 这里先要将其替换成国内的安装源, 找到: /etc/apt/sources.list文件, 先将其备份, 然后将里面的内容替换成以下内容:

deb http://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ jammy-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ jammy-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ jammy-proposed main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ jammy-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ jammy-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ jammy-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ jammy-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ jammy-backports main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse

替换完成后, 运行以下命令对软件包进行更新:

$ sudo apt update

(2).环境配置

假设登录系统账号为:suntiger, 代码工程保存在目录:
/home/linux-kernel-security-module, 在命令行下需要输入以下提权命令:

# chown -R suntiger /home/linux-kernel-security-module

执行该命令后, 在VScode中便可以正常创建代码文件。

在开发过程中, 需要使用Linux具体版本的内核头文件包, 通过以下命令进行安装:

# apt install linux-headers-$(uname -r)

还需要安装一些基本的开发工具, 例如:build-essential, 它包含了编译内核模块所需的编译器和其它工具, 安装命令如下:

# apt install build-essential
# add-apt-repository ppa:ubuntu-toolchain-r/test
# apt update
# apt install gcc-12 g++-12


开发一个简单内核模块扩展

首先在自己的工程目录新建一个代码文件:main.c, 然后写入以下代码:

#include <linux/init.h>
#include <linux/module.h>

static int __init construct(void) {
pr_info("First kernel module: Hello World!\n");
return 0;
}

static void __exit destruct(void) {
pr_info("First kernel module has been removed\n");
}

module_init(construct);
module_exit(destruct);

MODULE_LICENSE("GPL");

这段代码是一个简单的Linux内核模块开发实例, 以下部分将对这段代码的含义进行解读。

首先是头文件:

#include <linux/init.h>
#include <linux/module.h>

这些头文件包含了内核模块开发所需的函数和宏定义, 例如:

  • linux/init.h:提供了模块初始化和退出的宏。
  • linux/module.h:提供了内核模块的必要定义和函数。

下面来看这段代码:

static int __init construct(void) {
pr_info("First kernel module: Hello World!\n");
return 0;
}

这段代码是模块的初始化函数, 它在模块加载时被调用。函数名前面的__init是一个宏, 用于将函数标记为初始化代码。在模块加载时, 内核会调用这个函数。

  • pr_info是一个内核打印函数, 类似于用户空间的printf。它用于在内核日志中打印消息。
  • 该函数返回0, 表示初始化成功。

下面看另一段代码:

static void __exit destruct(void) {
pr_info("First kernel module has been removed\n");
}

这是模块的清理函数, 它在模块卸载时被调用。函数名前面的__exit是一个宏, 用于将函数标记为退出代码, 在模块卸载时,内核会调用这个函数。

看最后的三行代码:

module_init(construct);
module_exit(destruct);
MODULE_LICENSE("GPL");

前两行宏代码用于注册初始化和清理函数,其中:

  • module_init:宏注册了模块的初始化函数construct
  • module_exit宏注册了模块的清理函数destruct

最后一句宏定义了模块的许可证为GPL(GNU General Public License)。这表明该模块遵循GPL许可证。使用GPL许可证声明是强烈推荐的, 特别是当你要将模块发布为开源代码时。内核模块如果没有指定许可证, 内核将认为它是非GPL的, 并且可能限制某些符号的使用。

下面编写一个Makefile来尝试编译一下代码, 内容如下:

obj-m += main.o

all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

注意: Makefile的内容对格式有严格要求,make前面的空格必须是一个table空格,否则会引发编译错误。

第一句:obj-m += main.o定义了一个目标模块main.oobj-m是一个内核构建系统变量, 用于列出要构建的模块对象文件。通过将main.o添加到obj-m中, 这是为了告诉内核构建系统, 我们要编译main.c并生成main.o内核模块。

第二句:

all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

  • all是一个目标,它表示默认目标。如果在命令行中仅输入make, 这个目标就会被执行。
  • make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules是一个命令,用于调用内核构建系统来编译模块。
  • -C /lib/modules/$(shell uname -r)/build这个参数将工作目录切换到内核构建目录。$(shell uname -r)返回当前内核的版本号,例如:6.2.0-26-generic。因此这个命令切换到/lib/modules/6.2.0-26.generic/build目录,该目录包含了当前内核版本的构建环境。
  • M=$(PWD):这个参数告诉内核构建系统模块源代码所在的目录是当前目录, 表示当前工作目录。
  • modules: 这个目标指示内核构建系统生成模块。

下面切换到当前的工作目录, 在命令行中直接输入Make命令开始编译,编译过程如下:

如果编译成功, 在当前的工作目录会看到一个名为main.ko的内核模块,如图:

下面, 使用以下命令将编写的模块加载到内核中, 命令如下:

# insmod main.ko

接着输入lsmod命令查看已加载的内核模块,如图:

可以看到自己编写的内核模块已加载成功。

接着输入命令:dmesg | grep main -A1来查看模块的内核日志,如图:

可以看到加载的内核扩展模块日志成功打印, 说明已成功加载运行。

接下来, 尝试输入命令:rmmod main来卸载内核模块, 并使用命令dmesg | grep main -A2来查看内核日志, 如图:

从日志内容中可以发现卸载模块信息被打印, 说明内核扩展卸载成功, 使用命令lsmod再次查看内核模块,如图:

main内核扩展已完全卸载。


内核扩展中使用日志级别

在内核扩展中, 也可以打印不同日志级别的信息, 看下面的代码:

#include <linux/init.h>
#include <linux/module.h>

static int __init construct(void) {
printk(KERN_INFO "INFO Level message\n");
pr_info("Another INFO Level message\n");

printk(KERN_WARNING "WARNING Level message\n");
pr_warn("Another WARNING Level message\n");

printk(KERN_ERR "ERROR Level message\n");
pr_err("Another ERROR Level message\n");

printk(KERN_CONT "This " );
pr_cont("message ");
printk(KERN_CONT "is " );
pr_cont("single ");
printk(KERN_CONT "line\n");

return 0;
}

static void __exit destruct(void) {
printk(KERN_INFO "The module has been removed\n");
}

module_init(construct);
module_exit(destruct);

MODULE_LICENSE("GPL");

在上面的代码中, 主要是用了printk()和pr_xxx系列函数来打印不同日志级别的信息, 两者含义差不多。

  • printk()函数是内核中最基础的日志打印函数, 可以用于打印各种级别的日志信息,在使用时需要指定日志级别, 例如:KERN_INFOKERN_WARNINGKERN_ERR
  • pr_xxx系列函数是printk()的简化宏, 专门用于打印特定级别的日志信息, 主要简化了代码, 不需要显示指定日志级别。

编译该内核扩展并加载后, 可以使用以下命令查看错误级别的日志:

# dmesg --level err | grep message

返回信息如图:


开发网络数据包拦截内核扩展


有了上面的基础, 下面开发一个网络数据包拦截的内核扩展, 代码如下:

#include <linux/module.h> // included for all kernel modules
#include <linux/kernel.h> // included for KERN_INFO
#include <linux/init.h> // included for __init and __exit macros
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/netdevice.h>
#include <linux/vmalloc.h>

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("A Simple Hello Packet Module");

enum { NF_IP_PRE_ROUTING,
NF_IP_LOCAL_IN,
NF_IP_FORWARD,
NF_IP_LOCAL_OUT,
NF_IP_POST_ROUTING,
NF_IP_NUMHOOKS };

static struct nf_hook_ops in_nfho;
//net filter hook option struct
static struct nf_hook_ops out_nfho;
//net filter hook option struct

static void dump_addr(unsigned char *iphdr)
{
int i;
for(i=0; i<4; i++){
printk("%d.", *(iphdr+12+i));
}
printk(" -> ");
for(i=0; i<4; i++){
printk("%d.", *(iphdr+16+i));
}
printk("\n");
}

unsigned int my_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
printk("Hello packet! ");
//printk("from %s to %s\n", in->name, out->name);
unsigned char *iphdr = skb_network_header(skb);
if(iphdr){
dump_addr(iphdr);
}
return NF_ACCEPT;
// return NF_DROP;//会导致上不了网
}

static int __init init_func(void)
{
//NF_IP_PRE_ROUTING hook
in_nfho.hook = my_hook;
in_nfho.hooknum = NF_IP_LOCAL_IN;
in_nfho.pf = PF_INET;
in_nfho.priority = NF_IP_PRI_FIRST;

nf_register_net_hook(&init_net, &in_nfho);

//NF_IP_LOCAL_OUT hook
out_nfho.hook = my_hook;
out_nfho.hooknum = NF_IP_LOCAL_OUT;
out_nfho.pf = PF_INET;
out_nfho.priority = NF_IP_PRI_FIRST;

nf_register_net_hook(&init_net, &out_nfho);
return 0;
}

static void __exit exit_func(void)
{
nf_unregister_net_hook(&init_net, &in_nfho);
nf_unregister_net_hook(&init_net, &out_nfho);
printk(KERN_INFO "Cleaning up Hello_Packet module.\n");
}

module_init(init_func);
module_exit(exit_func);

在上面的代码中, 注册并设置了两个Netfilter钩子:

  • in_nfho钩子拦截进入本地网络栈的数据包(NF_INET_LOCAL_IN)
  • out_nfho钩子拦截离开本地网络栈的数据包(NF_INET_LOCAL_OUT)

最终通过dump_addr函数打印了数据包中的源IP地址和目的IP地址。

将代码按照上面的方法进行编译并加载, 模块加载完成后, 输入命令:

dmesg | tail

可以看到打印出来的源IP地址和目的IP地址,如图:

这里192.168.30.57是源IP地址,也就是我本地工作电脑地址; 192.168.201.204就是服务器地址。

相关推荐

梦幻诛仙12职业1亿元宝版本架设教程(包含资源下载)

架设教程:1;上传脚本zx到bin文件夹,给权限chmod-R777/bin2;输入zx安装宝塔面板输入对应序号,中途输入Y继续3;安装好之后,会给个宝塔地址及账号密码,复制地址...

Java零基础入门,科普Java你应该了解什么

最近很多人问我想学Java但是一点基础没有,网上看了一堆学习线路图还是无从下手。今天耗时3小时整理了一套保姆级的Java入门教程,建议收藏按照线路图一点点学习。一、Java的概况Java是1995年6...

01.Java发展历史(java发展历史简要)

1.Java发展历史Java由SunMicrosystems公司(现为Oracle公司)的JamesGosling及其团队在1991年开发,最初命名为"Oak",后改名为"...

Ubuntu16.04.1安装Java8(ubuntu终端安装java)

上篇文章讲解了怎么在Windows下安装Java8《Windows10安装Java8》,这里讲解下怎么在Linux下安装Java。由于之前已经安装了Ubuntu16.04.1《VmwareWorkst...

性能测试能力提升-JVM GC监控和优化

一、背景接着上一篇的知识:性能测试能力提升-JVMGC原理,本篇文章,我们将主要介绍JVMGC监控和优化相关的知识:命令行方式监控GC图形化方式监控GC什么时候需要开始GC优化?GC优化的目的GC...

Spring Boot Jar 包秒变 Docker 镜像实现多环境部署

你是否在互联网大厂后端开发工作中,遇到过这样的困扰?当完成一个SpringBoot项目开发,准备将Jar包部署到不同环境时,却发现各个环境依赖不同、配置复杂,部署过程繁琐又容易出错,不仅耗费...

「JDK 11」关于 Java 模块系统,看这一篇就够了

继2014年3月Java8发布之后,时隔4年,2018年9月,Java11如期发布,其间间隔了Java9和Java10两个非LTS(LongTermSupp...

对Java学习的10条建议(对java的认识和理解)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

JAVA入门教程-第1章 概述(java入门指南)

大道至简-JAVA入门教程在本教程中,你将学习Java语言的基础知识。Java基础内容涵盖:Java基础概念、Java词法结构、Java数组、Java流程控制、Java字符串、Java...

推荐一款Java音频视频编码器,很赞

Jave2是什么JAVE2(Java音频视频编码器)库是ffmpeg项目上的Java包装器。开发人员可以利用JAVE2将音频和视频文件从一种格式转码为另一种格式。在示例中,您可以将AVI文件转换为MP...

【JAVA教程】JAVA入门及开发环境安装

一、Java开发环境概述Java开发需要三个核心组件:JDK(JavaDevelopmentKit)-Java开发工具包,开发、编译、调试Java程序JRE(JavaRuntimeE...

Windows和Linux环境下的JDK安装教程

JavaDevelopmentKit(简称JDK),是Java开发的核心工具包,提供了Java应用程序的编译、运行和开发所需的各类工具和类库。它包括了JRE(JavaRuntimeEnviro...

记Tomcat优化方案(tomcat优化的几种方法)

Tomcat服务吞吐量评估方案问题:评估方案在一台8核16G的linux服务器上,使用tomcat容器部署服务。在正常情况下如何评估这个tomcat服务可处理的连接数,即服务的吞吐量,请在正常情况下考...

JVM GC诡异问题排查,k8s差点害死我……

前言本文将通过一个真实的生产环境案例,详细展示如何系统性地排查和解决JVM垃圾收集问题。这个案例涵盖了从问题发现、分析诊断到最终解决的完整过程,对于理解JVM调优实战具有重要的参考价值。系统背景我们的...

Thorium Reader - 功能强大的跨平台免费电子书阅读器

在日常阅读日益普及的今天,选择一款合适的电子书阅读器至关重要。ThoriumReader作为一款独具特色的阅读应用,正逐渐在众多同类产品中崭露头角,为用户带来了卓越的阅读体验。  跨平台的便捷性...