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

Linux内核如何替换内核函数并调用原始函数

ahcoder 2025-02-04 12:34 12 浏览

替换一个已经在内存中的函数,使得执行流流入我们自己的逻辑,然后再调用原始的函数,这是一个很古老的话题了。比如有个函数叫做funcion,而你希望统计一下调用function的次数,最直接的方法就是 如果有谁调用function的时候,调到下面这个就好了 :

void new_function()
{
	count++;
	return function();
}

网上很多文章给出了实现这个思路的Trick,而且一直以来计算机病毒也都采用了这种偷梁换柱的伎俩来实现自己的目的。然而,当你亲自去测试时,发现事情并不那么简单。

网上给出的许多方法均不再适用了,原因是在早期,这样做的人比较少,处理器和操作系统大可不必理会一些不符合常规的做法,但是随着这类Trick开始做坏事影响到正常的业务逻辑时,处理器厂商以及操作系统厂商或者社区便不得不在底层增加一些限制性机制,以防止这类Trick继续起作用。

常见的措施有两点:

  • 可执行代码段不可写

这个措施便封堵住了你想通过简单memcpy的方式替换函数指令的方案。

  • 内存buffer不可执行

这个措施便封堵住了你想把执行流jmp到你的一个保存指令的buffer的方案。

  • stack不可执行

别看这些措施都比较low,一看谁都懂,它们却避免了大量的缓冲区溢出带来的危害。

那么如果我们想用替换函数的Trick做正常的事情,怎么办?

我来简单谈一下我的方法。首先我不会去HOOK用户态的进程的函数,因为这样意义不大,改一下重启服务会好很多。所以说,本文特指HOOK内核函数的做法。毕竟内核重新编译,重启设备代价非常大。

我们知道,我们目前所使用的几乎所有计算机都是冯诺伊曼式的统一存储式计算机,即指令和数据是存在一起的,这就意味着我们必然可以在操作系统层面随意解释内存空间的含义。

我们在做正当的事情,所以我假设我们已经拿到了系统的root权限并且可以编译和插入内核模块。那么接下来的事情似乎就是一个流程了。

是的,修改页表项即可,即便无法简单地通过memcpy来替换函数指令,我们还是可以用以下的步骤来进行指令替换:

  1. 重新将函数地址对应的物理内存映射成可写;
  2. 用自己的jmp指令替换函数指令;
  3. 解除可写映射。

非常幸运,内核已经有了现成的 text_poke/text_poke_smp 函数来完成上面的事情。

同样的,针对一个堆上或者栈上分配的buffer不可执行,我们依然有办法。办法如下:

  1. 编写一个stub函数,实现随意,其代码指令和buffer相当;
  2. 用上面重映射函数地址为可写的方法用buffer重写stub函数;
  3. 将stub函数保存为要调用的函数指针。

是不是有点意思呢?下面是一个步骤示意图:

下面是一个代码,我稍后会针对这个代码,说几个细节方面的东西:

#include 
#include 
#include 
#include 
#include 
#define OPTSIZE	5
// saved_op保存跳转到原始函数的指令
char saved_op[OPTSIZE] = {0};
// jump_op保存跳转到hook函数的指令
char jump_op[OPTSIZE] = {0};
static unsigned int (*ptr_orig_conntrack_in)(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state);
static unsigned int (*ptr_ipv4_conntrack_in)(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state);
// stub函数,最终将会被保存指令的buffer覆盖掉
static unsigned int stub_ipv4_conntrack_in(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state)
{
	printk("hook stub conntrack\n");
	return 0;
}
// 这是我们的hook函数,当内核在调用ipv4_conntrack_in的时候,将会到达这个函数。
static unsigned int hook_ipv4_conntrack_in(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state)
{
	printk("hook conntrack\n");
	// 仅仅打印一行信息后,调用原始函数。
	return ptr_orig_conntrack_in(ops, skb, in, out, state);
}
static void *(*ptr_poke_smp)(void *addr, const void *opcode, size_t len);
static __init int hook_conn_init(void)
{
	s32 hook_offset, orig_offset;
	// 这个poke函数完成的就是重映射,写text段的事
	ptr_poke_smp = kallsyms_lookup_name("text_poke_smp");
	if (!ptr_poke_smp) {
		printk("err");
		return -1;
	}
	// 嗯,我们就是要hook住ipv4_conntrack_in,所以要先找到它!
	ptr_ipv4_conntrack_in = kallsyms_lookup_name("ipv4_conntrack_in");
	if (!ptr_ipv4_conntrack_in) {
		printk("err");
		return -1;
	}
	// 第一个字节当然是jump
	jump_op[0] = 0xe9;
	// 计算目标hook函数到当前位置的相对偏移
	hook_offset = (s32)((long)hook_ipv4_conntrack_in - (long)ptr_ipv4_conntrack_in - OPTSIZE);
	// 后面4个字节为一个相对偏移
	(*(s32*)(&jump_op[1])) = hook_offset;
	// 事实上,我们并没有保存原始ipv4_conntrack_in函数的头几条指令,
	// 而是直接jmp到了5条指令后的指令,对应上图,应该是指令buffer里没
	// 有old inst,直接就是jmp y了,为什么呢?后面细说。
	saved_op[0] = 0xe9;
	// 计算目标原始函数将要执行的位置到当前位置的偏移
	orig_offset = (s32)((long)ptr_ipv4_conntrack_in + OPTSIZE - ((long)stub_ipv4_conntrack_in + OPTSIZE));
	(*(s32*)(&saved_op[1])) = orig_offset;
	get_online_cpus();
	// 替换操作!
	ptr_poke_smp(stub_ipv4_conntrack_in, saved_op, OPTSIZE);
	ptr_orig_conntrack_in = stub_ipv4_conntrack_in;
	barrier();
	ptr_poke_smp(ptr_ipv4_conntrack_in, jump_op, OPTSIZE);
	put_online_cpus();
	return 0;
}
module_init(hook_conn_init);
static __exit void hook_conn_exit(void)
{
	get_online_cpus();
	ptr_poke_smp(ptr_ipv4_conntrack_in, saved_op, OPTSIZE);
	ptr_poke_smp(stub_ipv4_conntrack_in, stub_op, OPTSIZE);
	barrier();
	put_online_cpus();
}
module_exit(hook_conn_exit);
MODULE_DESCRIPTION("hook test");
MODULE_LICENSE("GPL");
MODULE_VERSION("1.1");

测试是OK的。

在上面的代码中,saved_op中为什么没有old inst呢?直接就是一个jmp y,这岂不是将原始函数中的头几个字节的指令给遗漏了吗?

其实说到这里,还真有个不好玩的Trick,起初我真的就是老老实实保存了前5个自己的指令,然后当需要调用原始ipv4_conntrack_in时,就先执行那5个保存的指令,也是OK的。随后我objdump这个函数发现了下面的代码:

     0000000000000380 :
      380:   e8 00 00 00 00          callq  385 
      385:   55                      push   %rbp
      386:   49 8b 40 18             mov    0x18(%r8),%rax
      38a:   48 89 f1                mov    %rsi,%rcx
      38d:   8b 57 2c                mov    0x2c(%rdi),%edx
      390:   be 02 00 00 00          mov    $0x2,%esi
      395:   48 89 e5                mov    %rsp,%rbp
      398:   48 8b b8 e8 03 00 00    mov    0x3e8(%rax),%rdi
      39f:   e8 00 00 00 00          callq  3a4 
      3a4:   5d                      pop    %rbp
      3a5:   c3                      retq
      3a6:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
      3ad:   00 00 00

注意前5个指令: e8 00 00 00 00 callq 385

可以看到,这个是可以忽略的。因为不管怎么说都是紧接着执行下面的指令。所以说,我就省去了inst的保存。

如果按照我的图示中常规的方法的话,代码稍微改一下即可:

char saved_op[OPTSIZE+OPTSIZE] = {0};
...
	// 增加一个指令拷贝的操作
	memcpy(saved_op, (unsigned char *)ptr_ipv4_conntrack_in, OPTSIZE);
	saved_op[OPTSIZE] = 0xe9;
	orig_offset = (s32)((long)ptr_ipv4_conntrack_in + OPTSIZE - ((long)stub_ipv4_conntrack_in + OPTSIZE + OPTSIZE));
	(*(s32*)(&saved_op[OPTSIZE+1])) = orig_offset;
...

但是以上的只是玩具。

有个非常现实的问题。在我保存原始函数的头n条指令的时候,n到底是多少呢?在本例中,显然n是5,符合如今Linux内核函数第一条指令几乎都是callq xxx的惯例。

然而,如果一个函数的第一条指令是下面的样子:

op d1 d2 d3 d4 d5

即一个操作码需要5个操作数,我要是只保存5个字节,最后在stub中的指令将会是下面的样子:

op d1 d2 d3 d4 0xe9 off1 off2 off3 off4

这显然是错误的,op操作码会将jmp指令0xe9解释成操作数。

解药呢?当然有咯。

我们不能鲁莽地备份固定长度的指令,而是应该这样做:

curr = 0
if orig[0] 为单字节操作码
	saved_op[curr] = orig[curr];
	curr++;
else if orig[0] 携带1个1字节操作数
	memcpy(saved_op, orig, 2);
	curr += 2;
else if orig[0] 携带2字节操作数
	memcpy(saved_op, orig, 3);
	curr += 3;
...
saved_op[curr] = 0xe9; // jmp
offset = ...
(*(s32*)(&saved_op[curr+1])) = offset;

这是正确的做法。

需要C/C++ Linux服务器架构师学习资料私信“资料”(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

相关推荐

KaOS 2025.05版本发布:全面拥抱Qt6,彻底告别Qt5

KaOSLinux2025.05版本重磅发布:全面拥抱Qt6,开启KDE生态新篇章继2025.03版本发布两个月后,专注于KDE桌面环境、采用XFS文件系统的滚动发行版Li...

基于FIMC接口的CMOS摄像头驱动分析与设计

摘要:目前的嵌入式系统中,USB摄像头使用比较普遍,但其应用会受到传输速度的限制。本文采用一款高速CMOS摄像头,其驱动利用S3C6410内置的FIMC接口技术,采用DMA和ping-pong缓冲...

没错是微软 推出基于Linux的交换机系统

2015-09-2205:59:59作者:郑伟你没看错,为了提升自身Azure云数据中心内网络设备的兼容性及开放性,微软也开始推出基于Linux的网络交换机系统了。这个被称为AzureCloud...

Linus Torvalds 宣布首个 Linux 内核 6.16 候选版本

Linux内核负责人兼创始人LinusTorvalds宣布关闭合并窗口,该窗口用于将主要新功能添加到内核中,并开始发布Linux6.16候选版本,从候选版本1(Linux6.16-r...

Linux内核漏洞将影响Haswell架构服务器

在infoq网站上,GilTene最近报告一个十分重要,但并不为人知Linux内核补丁,特别对采用Haswell架构的Linux系统用户和管理员应该特别关注。报告提醒RedHat发行版的用户(包括...

关于Linux性能调优中网络I/O的一些笔记

写在前面和小伙伴分享一些Linux网络优化的笔记,内容很浅,可以用作入门博文内容结合《Linux性能优化》读书笔记整理涉及内容包括常用的优化工具(mii-tool,ethtool,ifconfig,i...

国产操作系统- Veket Linux(国产操作系统之光银河麒麟阅读理解)

VeketLinux是一个随身的可装在U盘的Linux操作系统。主要面向桌面用户。它的设计重点是提供简单易用且稳定的操作系统,同时保持更新和开发。它具有强大的功能集和广泛的用户基础,可满足...

AlmaLinux 9.6发布:升级工具、初步支持IBM Power虚拟化技术

IT之家5月21日消息,科技媒体linuxiac昨日(5月20日)发布博文,报道称代号为SageMargay的AlmaLinux9.6发行版已上线,距上一版本9.5发...

跟老韩学Linux运维架构师系列,vim与view的基本使用

下面是vim和view的10个实例:用vim打开一个新文件:vimnewfile.txt这个命令将会在vim编辑器中打开一个新文件。在vim中移动光标:使用方向键或h、j、k、l键来移动光标。在v...

malloc底层原理剖析——ptmalloc内存池

malloc底层为什么是内存池malloc大家都用过,其是库函数。我们都知道库函数在不同的操作系统中其实执行的是系统调用,那么malloc在Linux上执行的是哪个系统调用呢?brk()和mmap()...

Zen 6架构首秀Linux,AMD加速下一代处理器布局

IT之家5月15日消息,科技媒体Phoronix昨日(5月14日)发布博文,报道称AMD已经开始为下一代“Zen6”处理器做准备,已为该构架向Linux内核提交了首个补丁,...

为何越来越多企业转向安卓/Linux工业平板电脑?答案在这里

在工业领域,设备的稳定性至关重要,尤其是工业平板电脑,常年运行在高温、粉尘、潮湿等复杂环境下,一旦系统崩溃或者卡顿,可能会影响整个生产流程。那么,为什么越来越多的企业选择安卓/Linux工业平板电脑,...

从3ms到0.8ms:ARM+Linux如何重塑工业控制实时性标杆

在智能制造领域,产线控制系统对实时性的要求越来越高。根据行业调研数据,超过65%的工业现场出现过因系统响应延迟导致的故障停机,平均每次停机造成的直接损失高达2-8万元。传统x86架构搭配Windows...

看Linux如何"挖坑种树"

写在前面,有人看我的Linux文章说技术难度不深,笔者不是不想写深,笔者是觉得Linux难就难在入门,入门之后你就知道如何上网查询你所要要解决的Linux需求。如果你已入门,此文已对你无用,请略过此...

AlmaLinux 9.6 发布,新增功能亮点纷呈!

距离上一版本AlmaLinux9.5发布六个月后,基于5.14内核的AlmaLinux正式宣布其企业级Linux发行版的9.x系列第六个更新——AlmaLinux9.6(Sag...