linux 内核热更换,如何替换一个Linux内核函数的实现-热补丁原理

35b3deeaa90e49d6f32093533e9f4db7.png

看题目, 替换Linux内核函数的实现 ,what?这不就是kpatch嘛!也就是我们所谓的 热补丁 。我们为内核做热补丁的时候,没人用汇编写吧,没人用二进制指令码去拼逻辑吧,我们一般都是直接修改内核函数的C代码的,然后形成一个patch文件,然后…然后…去读kpatch的Documents吧。

本文我将要描述的是热补丁的原理,而不是一个如何使用kpatch的Howto,更不是关于任何kpatch技术的源码分析。

以一个实际的3.10内核的Bugfix热补丁为例开始我们的故事。

在该实例中,我们修改了set_next_buddy的实现:

diff--git a/kernel/sched/fair.c b/kernel/sched/fair.c

...

@@-4537,8+4540,11@@staticvoidset_next_buddy(structsched_entity*se)

if(entity_is_task(se)&&unlikely(task_of(se)->policy==SCHED_IDLE))

return;

-for_each_sched_entity(se)

+for_each_sched_entity(se){

+if(!se->on_rq)

+return;

cfs_rq_of(se)->next=se;

+}

}

看来,为了Fix一个已知的Bug,我们需要为set_next_buddy函数加几行代码,很显然,这很容易。

增加了这几行代码后,便形成了一个新的set_next_buddy函数,为了能让新的函数run起来,现在我们面临三个问题:

我们如何可以将这个新的set_next_buddy函数编译成二进制?

我们如何将这个新的set_next_buddy函数二进制码注入到正在运行的内核?

我们如何用新的set_next_buddy二进制替换老的set_next_buddy函数?

我们一个一个问题看。

首先,第一个问题非常容易解决。

我们修改了一个C文件kernel/sched/fair.c,为了解决编译时的依赖问题,只需要将修改后形成的patch文件打入当前运行内核的源码树中就可以编译了,通过objdump之类的机制,我们可以把编译好的set_next_buddy二进制抠出来形成一个obj文件,然后组成一个ko就不是什么难事了。这便形成了一个内核模块,类似 kpatch-y8u59dkv.ko

接下来看第二个问题,如何将第一个问题中形成的ko文件中set_next_buddy二进制注入到内核呢?

这也不难,kpatch的模块加载机制就是干这个的。打入热补丁的内核就会出现两个set_next_buddy函数:

crash>dis set_next_buddy

dis:set_next_buddy:duplicate text symbols found:

// 老的set_next_buddy

ffffffff810b9450(t)set_next_buddy/usr/src/debug/kernel-3.10.0/linux-3.10.0.x86_64/kernel/sched/fair.c:4536

// 新的set_next_buddy

ffffffffa0382410(t)set_next_buddy[kpatch_y8u59dkv]

到了第三个问题,有点麻烦。新的set_next_buddy二进制如何替换老的set_next_buddy二进制呢?

显然,不能采用覆盖的方式,因为内核函数的布局是非常紧凑的且连续的,每个函数的空间在内核形成的时候就确定了,如果新函数比老函数大很多,就会越界覆盖掉其它的函数。

采用我前面文章里描述的二进制hook技术是可行的,比如下面文章里的方法:https://blog.csdn.net/dog250/article/details/105206753通过二进制diff,然后紧凑地poke需要修改的地方,这无疑是一种妙招!然而这种方法并不优雅,充满了奇技淫巧,它最大的问题就是逆经理。

最正规的方法就是使用ftrace的hook,即 修改老函数的开头5个字节的ftrace stub,将其修改成“jmp/call 新函数”的指令,并且在stub函数中skip老函数的栈帧。 如此一来彻底绕过老的函数。

我们来看上面提到的两个set_next_buddy的二进制:

// 老的set_next_buddy:

crash>dis ffffffff810b94504

// 注意,老函数的ftrace stub已经被替换

0xffffffff810b9450:callq0xffffffff81646df0

// 后面这些如何被绕过呢?ftrace_regs_caller返回后如何被skip掉呢?这需要平衡堆栈的技巧!

// 后面通过实例来讲如何平衡堆栈,绕过老的函数。

0xffffffff810b9455:push%rbp

0xffffffff810b9456:cmpq $0x0,0x150(%rdi)

0xffffffff810b945e:mov%rsp,%rbp

// 新的set_next_buddy:

crash>dis ffffffffa03824104

// 新函数则是ftrace_regs_caller最终要调用的函数

0xffffffffa0382410:nopl0x0(%rax,%rax,1)[FTRACE NOP]

0xffffffffa0382415:push%rbp

0xffffffffa0382416:cmpq $0x0,0x150(%rdi)

0xffffffffa038241e:mov%rsp,%rbp

这就是热补丁的原理了。

本文到这里都是纸上的高谈阔论,就此结束未免尴尬且遗憾,接下来我要用一个实际的例子来说明这一切。这个例子非常简单,随便摆置几下就能run起来看到效果。

我比较讨厌源码分析,所以我不会去走读注释ftrace_regs_caller的源码,我用我自己的方式来实现类似的需求,并且要简单的多,这非常有利于咱们工人理解事情的本质。

我的例子不会去patch内核中既有的函数,我的例子patch的是我编写的一个简单的内核模块里的函数,该模块代码如下:

#include

#include

// 下面的sample_read就是我将要patch的函数

staticssize_tsample_read(structfile*file,char__user*ubuf,size_tcount,loff_t*ppos)

{

intn=0;

charkb[16];

if(*ppos!=0){

return0;

}

n=sprintf(kb,"%d\n",1234);

memcpy(ubuf,kb,n);

*ppos+=n;

returnn;

}

staticstructfile_operations sample_ops={

.owner=THIS_MODULE,

.read=sample_read,

};

staticstructproc_dir_entry*ent;

staticint__initsample_init(void)

{

ent=proc_create("test",0660,NULL,&sample_ops);

if(!ent)

return-1;

return0;

}

staticvoid__exitsample_exit(void)

{

proc_remove(ent);

}

module_init(sample_init);

module_exit(sample_exit);

MODULE_LICENSE("GPL");

我们加载它,然后去read一下/proc/test:

[root@localhost test]#insmod sample.ko

[root@localhost test]#cat/proc/test

1234

OK,一切如愿。此时,我们看看sample_read的前面的5个字节:

crash>dis sample_read1

0xffffffffa038c000:nopl0x0(%rax,%rax,1)[FTRACE NOP]

来来来,在已经加载了sample.ko的前提下,我们现在patch它。我的目标是,Fix掉sample_read函数,使得它返回4321而不是1234。

以下是全部的代码,要点都在注释里:

// hijack.c

#include

#include

#include

char*stub;

char*addr=NULL;

// 可以用JMP模式,也可以用CALL模式

//#define JMP 1

// 和sample模块里同名的sample_read函数

staticssize_tsample_read(structfile*file,char__user*ubuf,size_tcount,loff_t*ppos)

{

intn=0;

charkb[16];

if(*ppos!=0){

return0;

}

// 这里我们把1234的输出给fix成4321的输出

n=sprintf(kb,"%d\n",4321);

memcpy(ubuf,kb,n);

*ppos+=n;

returnn;

}

// hijack_stub的作用就类似于ftrace kpatch里的ftrace_regs_caller

staticssize_thijack_stub(structfile*file,char__user*ubuf,size_tcount,loff_t*ppos)

{

// 用nop占位,加上C编译器自动生成的函数header代码,这么大的函数来容纳stub应该够了。

asm("nop; nop; nop; nop; nop; nop; nop; nop;");

return0;

}

#defineFTRACE_SIZE5

#definePOKE_OFFSET0

#definePOKE_LENGTH5

#defineSKIP_LENGTH8

staticunsignedlong*(*_mod_find_symname)(structmodule*mod,constchar*name);

staticvoid*(*_text_poke_smp)(void*addr,constvoid*opcode,size_tlen);

staticstructmutex*_text_mutex;

unsignedcharsaved_inst[POKE_LENGTH];

structmodule*mod;

staticint__inithotfix_init(void)

{

unsignedcharjmp_call[POKE_LENGTH];

unsignedchare8_skip_stack[SKIP_LENGTH];

s32 offset,i=5;

mod=find_module("sample");

if(!mod){

printk("没加载sample模块,你要patch个啥?\n");

return-1;

}

_mod_find_symname=(void*)kallsyms_lookup_name("mod_find_symname");

if(!_mod_find_symname){

printk("还没开始,就已经结束。");

return-1;

}

addr=(void*)_mod_find_symname(mod,"sample_read");

if(!addr){

printk("一切还没有准备好!请先加载sample模块。\n");

return-1;

}

_text_poke_smp=(void*)kallsyms_lookup_name("text_poke_smp");

_text_mutex=(void*)kallsyms_lookup_name("text_mutex");

if(!_text_poke_smp||!_text_mutex){

printk("还没开始,就已经结束。");

return-1;

}

stub=(void*)hijack_stub;

offset=(s32)((long)sample_read-(long)stub-FTRACE_SIZE);

// 下面的代码就是stub函数的最终填充,它类似于ftrace_regs_caller的作用!

e8_skip_stack[0]=0xe8;

(*(s32*)(&e8_skip_stack[1]))=offset;

#ifndefJMP// 如果是call模式,则需要手工平衡堆栈,跳过原始函数的栈帧

e8_skip_stack[i++]=0x41;// pop %r11

e8_skip_stack[i++]=0x5b;// r11寄存器为临时使用寄存器,遵循调用者自行保护原则

#endif

e8_skip_stack[i++]=0xc3;

_text_poke_smp(&stub[0],e8_skip_stack,SKIP_LENGTH);

offset=(s32)((long)stub-(long)addr-FTRACE_SIZE);

memcpy(&saved_inst[0],addr,POKE_LENGTH);

#ifndefJMP

jmp_call[0]=0xe8;

#else

jmp_call[0]=0xe9;

#endif

(*(s32*)(&jmp_call[1]))=offset;

get_online_cpus();

mutex_lock(_text_mutex);

_text_poke_smp(&addr[POKE_OFFSET],jmp_call,POKE_LENGTH);

mutex_unlock(_text_mutex);

put_online_cpus();

return0;

}

staticvoid__exithotfix_exit(void)

{

mod=find_module("sample");

if(!mod){

printk("一切已经结束!\n");

return;

}

addr=(void*)_mod_find_symname(mod,"sample_read");

if(!addr){

printk("一切已经结束!\n");

return;

}

get_online_cpus();

mutex_lock(_text_mutex);

_text_poke_smp(&addr[POKE_OFFSET],&saved_inst[0],POKE_LENGTH);

mutex_unlock(_text_mutex);

put_online_cpus();

}

module_init(hotfix_init);

module_exit(hotfix_exit);

MODULE_LICENSE("GPL");

OK,我们载入它吧,然后重新read一下/proc/test:

[root@localhost test]#insmod./hijack.ko

[root@localhost test]#cat/proc/test

4321

可以看到,已经patch成功。到底发生了什么?我们看下反汇编:

crash>dis sample_read

dis:sample_read:duplicate text symbols found:

ffffffffa039d000(t)sample_read[sample]

ffffffffa03a2020(t)sample_read[hijack]

crash>

嗯,已经有两个同名的sample_read函数符号了,sample模块里的是老的函数,而hijack模块里的是新的fix后的函数。我们分别看一下:

// 先看老的sample_read,它的ftrace stub已经被改成了call hijack_stub

crash>dis ffffffffa039d0001

0xffffffffa039d000:callq0xffffffffa03a2000

// 再看新的sample_read,它就是最终被执行的函数

crash>dis ffffffffa03a20201

0xffffffffa03a2020:nopl0x0(%rax,%rax,1)[FTRACE NOP]

crash>

当新的sample_read执行完毕,返回hijack_stub后,如果是CALL模式,此时需要skip掉老的sample_read函数的栈帧,所以一个pop %r11来完成它,之后直接ret即可,如果是JMP模式,则直接ret,不需要skip栈帧,因为JMP指令根本就不会压栈。

好了,这就是我要讲的故事。说白了,本文描述的依然是一个手艺活,我只是希望用大家都能理解的最简单的方式,来展示相对比较复杂的热补丁的实现原理。我觉得工友们有必要对底层的原理有深刻的认知。

经理也爱吃辣椒,但不很,不过显而易见的是,经理洒不了水。

浙江温州皮鞋湿,下雨进水不会胖!

版权声明:本文为CSDN博主「dog250」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。

原文链接:

https://blog.csdn.net/dog250/article/details/105254739

-END-

免责声明:整理文章为传播相关技术,版权归原作者所有,如有侵权,请联系删除

5da0d5f20628851bd2739a67334750db.png

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第一部分 基础知识 <br>1.1 什么是LKMs <br>1.2 什么是系统调用 <br>1.3 什么是内核符号表(Kernel-Symbol-Table) <br>1.4 如何实现从用户空间到内核空间的转换 <br>1.5 使用用户空间函数的方法 <br>1.6 常用内核空间函数列表 <br>1.7 什么是内核守护进程 <br>1.8 创建你自己的设备 <br><br>第二部分 渐入佳境 <br>2.1 如何截获系统调用 <br>2.2 一些有趣的系统调用 <br>2.2.1 发现有趣的系统调用(strace方法) <br>2.3 迷惑内核的系统表 <br>2.4 和文件系统有关的攻击 <br>2.4.1 如何隐藏文件 <br>2.4.2 如何隐藏文件的内容(完全的) <br>2.4.3 如何隐藏文件的某一部分(一个实现原型) <br>2.4.4 如何重新定向或者监视文件操作 <br>2.4.5 如何避免任何文件权限问题 <br>2.4.6 如何使的一个有入侵工具的目录不可存取 <br>2.4.7 如何改变CHROOT环境 <br>2.5 和进程有关的入侵 <br>2.5.1 如何隐藏任何进程 <br>2.5.2 如果改变文件的执行结果 <br>2.6 和网络(Socket)有关的入侵 <br>2.6.1 如果控制Socket操作 <br>2.7 TTY纪录的方法 <br>2.8 用LKMs写病毒 <br>2.8.1 如何让LKM病毒感染任何文件(不仅仅是模块) <br>2.8.2 如何让LKM病毒帮助我们进入系统 <br>2.9 使我们的LKM不可见,而且不可卸载 <br>2.10 其他的入侵kerneld进程的方法 <br>2.11 如何检查当前的我们的LKM <br><br>第三部分 解决方案(给系统管理员) <br>3.1 LKM检测的理论和想法 <br>3.1.1 一个使用的检测器的原形 <br>3.1.2 一个密码保护的create_module(...)的例子 <br>3.2 防止LKM传染者的方法 <br>3.3 使你的程序不可以被跟踪(理论) <br>3.3.1 一个反跟踪的实用例子 <br>3.4 使用LKMs来防护你的linux内核 <br>3.4.1 为什么我们必须允许任何一个程序都拥有可执行的权限 <br>3.4.2 链接的补丁 <br>3.4.3 /proc权限的补丁 <br>3.4.4 安全级别的补丁 <br>3.4.5 底层磁盘补丁 <br><br>第四部分 一些更好的想法(给hacker的) <br>4.1 击败系统管理员的LKM的方法 <br>4.2 修补整个内核-或者创建Hacker-OS <br>4.2.1 如何在/dev/kmem中找到内核符号表 <br>4.2.2 新的不需要内核支持的'insmod' <br>4.3 最后的话 <br><br>第五部分 最近的一些东西:2.2.x版本的内核 <br>5.1 对于LKM作者来说,一些主要的不同点 <br><br>第六部分 最后的话 <br>6.1 LKM传奇以及如何使得一个系统即好用又安全 <br>6.2 一些资源链接
linux内核调试分析指南 linux内核调试分析指南--上篇 本文档已经转到下面的网址,位于zh-kernel.org的文档停止更新,请访问新网址 一些前言 作者前言 知识从哪里来 为什么撰写本文档 为什么需要汇编级调试 ***第一部分:基础知识*** 总纲:内核世界的陷阱 源码阅读的陷阱 代码调试的陷阱 原理理解的陷阱 建立调试环境 发行版的选择和安装 安装交叉编译工具 bin工具集的使用 qemu的使用 skyeye的使用 UML的使用 vmware的使用 initrd.img的原理与制作 x86虚拟调试环境的建立 arm虚拟调试环境的建立 arm开发板调试环境的建立 gdb基础 基本命令 gdb之gui gdb技巧 gdb宏 汇编基础--X86篇 用户手册 AT&T汇编格式 内联汇编 汇编与C函数的相互调用 调用链形成和参数传递 C难点的汇编解释 优化级别的影响 汇编基础--ARM篇 用户手册 调用链形成和参数传递 源码浏览工具 调用图生成工具 find + grep wine + SI global Source-Navigator vim + cscope/ctags kscope lxr SI等与gdb的特点 调用链、调用树和调用图 理想调用链 函数指针调用 调用链的层次 非理想调用链 调用树与调用图 穿越盲区 穿越gdb的盲区 穿越交叉索引工具的盲区 工程方法 bug 与 OOPS linux内核调试分析指南--下篇 ***第二部分:内核分析*** 内核组织层次和复杂度 内核层次 内核复杂度 复杂度隔离 gdb在内核分析中的用途 数据验证 界面剥离 参数记忆 路径快照 长程跟踪 整理思路 内核编码的艺术 信息聚集 数据聚集 关系聚集 操作聚集 松散聚集 顺序聚集 链表聚集 哈希聚集 树形聚集 分层聚集 分块聚集 对象聚集 设施客户 设备驱动模型分析 linux设备子系统的组成 设备驱动模型 usb子系统分析 如何阅读分析大型子系统 btrfs文件系统分析 区间树核心代码分析 B树核心代码分析 调试相关子系统 kgdb源码分析 sysrq oprofile kprobes 驱动分析 载入模块符号 ***第三部分:其他工具*** kexec strace ltrace SystemTap MEMWATCH YAMD Magic SysRq 附录:社区交流相关 补丁提交相关文档 补丁制作与提交示范 多补丁发送工具 git使用 Git公共库创建及使用 附录:内核参考书籍文章 内核git库 书籍 子系统官方网站 必看网站 参考文章 私人备忘
Linux内核中的无锁(lock-free)技术主要用于实现高效的并发数据结构,以提高系统的性能和吞吐量。其中,无锁环形缓冲区(lock-free ring buffer)是一种常用的数据结构,它可以高效地实现在多个线程之间传递数据的功能。 无锁环形缓冲区的实现原理如下: 1. 环形缓冲区的数据结构:无锁环形缓冲区由一个固定大小的环形数组和两个指针构成,一个是读指针,一个是写指针。读指针指向下一个将要读取的元素,写指针指向下一个将要写入的元素。 2. 原子操作:无锁环形缓冲区的实现依赖于原子操作(atomic operations),这些操作是在单个CPU指令中执行的,不会被其他线程中断。在Linux内核中,原子操作是通过宏定义实现的,如“atomic_add()”、“atomic_sub()”等。 3. 写入数据:当一个线程想要写入数据时,它首先需要检查缓冲区是否已满。如果缓冲区已满,则写入操作失败。如果缓冲区未满,则该线程会使用原子操作将数据写入缓冲区,并更新写指针。 4. 读取数据:当一个线程想要读取数据时,它首先需要检查缓冲区是否为空。如果缓冲区为空,则读取操作失败。如果缓冲区不为空,则该线程会使用原子操作将数据从缓冲区中读取,并更新读指针。 5. 线程同步:无锁环形缓冲区的实现不依赖于任何锁机制,因此可以避免锁竞争和死锁等问题。不过,在多个线程并发读写的情况下,需要使用一些同步机制来保证线程安全,如使用原子操作或者memory barrier等技术。 总的来说,无锁环形缓冲区是一种高效的并发数据结构,能够在多个线程之间高效地传递数据,提高系统的性能和吞吐量。在Linux内核中,无锁环形缓冲区的实现依赖于原子操作和线程同步技术,可以避免锁竞争和死锁等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值