简介
内核热补丁是一种无需重启操作系统,动态为内核打补丁的技术。系统管理员基于该技术,可以在不重启系统的情况下,修复内核BUG或安全漏洞,可以在最大程度上减少系统宕机时间,增加系统的可用性。
使用场景
-
修复内核或模块的缺陷函数
内核热补丁能够动态的修复内核和模块的缺陷函数。在开发人员发现问题,或者操作系统发现安全漏洞需要修复时,可以通过将缺陷函数或者安全补丁制作成内核热补丁打入系统中的方法,在不需要重启系统或者插拔模块、不中断业务的前提下修复缺陷。 -
开发过程中增加调试或测试手段
内核热补丁也适用于在开发过程中进行调试和测试。比如在模块或者内核的开发过程中,如果需要通过在某一个函数中添加打印信息,或者为函数中某一个变量赋予特定的值,可以通过内核热补丁的形式实现,而不需要重新编译内核、安装、重启的操作。
原理分析
Kpatch基于ftrace实现内核函数的替换,类似于ftrace的动态探测点。利用mcount机制,在内核编译时在每个函数入口保留数个字节,然后在打补丁时将“被替换函数”入口保留的字节替换为跳转指令,跳转到Kpatch的相关流程中,最终进入“新函数”的执行流程,实现函数级别的执行流程在线替换。具体而言如下图:
image.png01. 准备kpatch
1.先从github上把kpatch工具下载一下
git clone GitHub - dynup/kpatch: kpatch - live kernel patching
2.进入到kpacth目录执行买这一步会安装好kpatch所需的依赖:
source test/integration/lib.sh
# Will request root privileges
kpatch_dependencies
- 执行make && make install,编译和安装kpatch
02. 准备内核编译环境
kpatch制作需要编译内核和内核模块,所以需要内核源码包以及编译内核所需的依赖。
- 下载centos debuginfo包
wget http://debuginfo.centos.org/8/x86_64/Packages/kernel-debuginfo-4.18.0-305.3.1.el8.x86_64.rpm
wget http://debuginfo.centos.org/8/x86_64/Packages/kernel-debuginfo-common-x86_64-4.18.0-305.3.1.el8.x86_64.rpm
这俩全部装上后还需要一个内核源码包:kernel-4.18.0-305.10.2.el8_4.src.rpm
网上也能找到,安装上之后就可以有内核源码编译环境了。内核源码位置在/root/rpmbuild/SOURCES/linux-4.18.0-305.10.2.el8_4
-
更新gcc版本
这一步是因为内核编译的 gcc版本和你系统安装的可能不是一个版本。所以需要讲系统安装的gcc更新到内核编译的那个版本。一般都是低的版本,所以这里不是做升级哦。先把系统安装的gcc卸载掉(如果有的话),从网上找对应的gcc版本。一般需要安装cpp、gcc、libgomp 三个包 -
安装内核编译需要的包
yum install patch bison flex openssl openssl-devel
这几个是编译过程需要安装的 -
修改内核配置
还需要修改一下内核配置,要不然编译报错:
vim /boot/config-xxxxx
找到这个CONFIG_SYSTEM_TRUSTED_KEYS,修改为:CONFIG_SYSTEM_TRUSTED_KEYS=“”
这里可以先备份一下,然后修改备份文件比较好。到时候制作补丁的时候就可以直接指定备份的文件
举例说明
-
加载和卸载内核热补丁
insmod livepatch_xxx.ko
像加载驱动模块一样加载就行或者使用kpatch load livepatch_xx.ko
-
卸载补丁则需要用
kpatch unload livepatch_xx
- 以如下patch为例:
-
将步骤1中的patch制作为对应ko文件,执行命令及结果显示如下:
image.png
说明:关于make-kpatch命令的执行参数说明可执行"make-kpatch --help"命令来查看
-
执行如下命令,打入内核热补丁:
image.png -
查看对应dmesg信息如下:
- 执行查看内存信息命令,查看到结果以及dmesg信息如下所示:
image.png
结合步骤1中的代码对比发现,在/proc/meminfo所看到的内容中,多了LivePatchtest项,并且在执行"cat /proc/meminfo"命令时,dmesg信息中也打印了步骤1中patch代码打印片段。这就证明了我们制作的热补丁是有效的,成功打入了当前运行的内核中。
原理剖析
社区版本的Kpatch目前只支持X86_64和PPC64le两种架构,原理上是采用ftrace技术实现内核函数的替换,类似于ftrace的动态探测点,不过这里并不是统计数据的运行,而是修改函数的运行序列,以此来跳过旧函数直接运行到新函数。
图1 热补丁原理框架图对于Kpatch的代码架构,主要由4个组件模块组成:
- kpatch-build:编译组件,主要通过编译对比,将source diff patch转换成patch module;
- kpatch core module:核心组件,为hot patch注册新的函数以用于替换提供接口的内核模块,它使用内核ftrace子系统来挂钩原始函数的mcount调用命令,以便将对原始函数的调用重定位到替换函数;
- patch module:用来提供替代函数及原始函数元数据的内核模块;
- kpatch utility:命令组件,允许用于管理hot patch模块的命令行工具,主要由kpatch/kpatch/kpatch脚本实现kpatch install、kpatch load等相关命令。
了解完系统原理和代码架构后,你肯定会有以下疑惑:kpatch是如何生成一个可动态加载/卸载的模块的?当模块安装后,新函数是如何替换旧函数并开始工作的?接下来,将对这些疑问进行一一解答。
2.1 如何生成一个模块?
主要依赖于kpatch-build组件,它的大部分工作是由kpatch/kpatch-build/kpatch-build脚本执行,该脚本通过create-diff-object文件去比较更改的对象,从而使得用户可以在用户态可以直接以kpatch-build命令的方式直接将源级别的diff补丁文件转换为内核补丁模块。其编译的主要过程如下:
- 测试补丁文件;
- 编译内核源码文件;
- 编译打上补丁的源码文件;
- 通过两次编译目标文件的变动情况,提取生成的二进制代码diff.o;
- 通过二进制文件,最终生成内核模块。
2.2 新函数如何替换旧函数?
主要依赖于kpatch core module组件,该组件提供了一个kpatch_register()接口,它的主要作用就是完成新函数(要打补丁的函数)对旧函数的替换。那么这个函数到底做了什么来实现新旧函数的替换呢?
image.png图2 kpatch_register()函数调用逻辑
虽然kpatch_register()执行的调用很多,但是真正重要的函数却只是kpatch_link_object()和stop_machine()。后者想必熟悉Linux进程调度的人都会知道,它是一个通信信号,用来暂停相关状态的运行。该机制被运用在CPU热插拔、内存热插拔、ftrace、增加/删除模块等应用,而Kpatch则是调用stop_machine()来确保当前状态没有线程调用旧函数,以此来保证系统的安全性。而对于前者,它却是Kpatch技术的重点,因为是它完成了新旧函数的替换,接下来为了方便理解,先从其数据结构说起。
2.2.1 数据结构
kpatch_register()函数中,涉及到的数据结构主要有struct kpatch_module、struct kpatch_object、struct kpatch_func、struct kpatch_dynrela,那么它们之间的关系是啥呢?
图3 结构体调用关系图
通过图3可以很直观的看到如何从struct kpatch_module结构体获取到struct kpatch_func中的新旧函数地址,代码中采用了list_for_each_entry()进行了变量的提取,代码如下。
图4 通过kpmod提取object和func
2.2.2 替换核心kpatch_link_object()
该函数连接到要打补丁的对象,并准备打补丁,它主要完成了目标模块查找、补丁模块的重定位、计算打补丁函数的地址、使用ftrace进行注册等功能,函数具体调用逻辑如图5所示。
图5 kpatch_link_object()函数调用逻辑1)kpatch_find_object_symbol()
该函数用于计算旧函数的地址,因为定义中并没有给出旧函数的地址,主要是通过调用kallsyms_on_each_symbol()遍历符号列表来得到旧函数地址。
2)ftrace_set_filter_ip()、register_ftrace_function()
通过这两个函数可以看出,引入了ftrace框架,接下来先简单介绍一下。Ftrace是一个用于在函数级别跟踪内核的框架,当定义了CONFIG_FUNCTION_TRACER后,内核代码gcc编译时会加入-pg选项,每个函数的开头都会插入一个特殊的跟踪函数调用mcount()进行插桩操作,不过从每个函数调用ftrace是非常昂贵的,会严重影响系统的性能,因此动态ftrace应运而生。
动态探测点的基本原理是:利用mcount机制,当内核编译时,在每个函数入口保留数个字节,应该都为nop指令。在使用ftrace时,根据.config配置文件中是否定义了CONFIG_DYNAMIC_FTRACE_WITH_REGS将保留的字节替换成ftrace_caller()或ftrace_regs_caller(),而它们的函数内部所调用的ftrace_stub()会被替换成ftrace_ops_no_ops或ftrace_ops_list_func(),然后调用__ftrace_ops_list_func()去遍历ftrace_ops_list链表,提取并跳转到需要的执行探测操作的代码。
图6 动态ftrace hook结构体与函数调用框图
看到这里,你肯定会问,内核热补丁既然是利用动态ftrace方式将新旧函数替换掉,那么新函数是如何被加入到哈希表中并能够被调用呢?这又和ftrace_set_filter_ip()、register_ftrace_function()两个函数有什么关系呢?其实,这两个函数其实是对ftrace hook功能的一种封装,便于用户可以直接使用。
ftrace_set_filter_ip()的主要功能是使能HOOK点,将新的函数加入到struct ftrace_ops结构体下的hash表中。
register_ftrace_function()的主要功能则是将新的HOOK函数加入到ftrace_ops_list链表中,从而使得__ftrace_ops_list_func()去遍历ftrace_ops_list链表时能找到新的IP函数。
3).func = kpatch_ftrace_handler
既然register_ftrace_function(struct ftrace_ops *ops)的作用是将kpatch_ftrace_ops进行了链表添加,那么我们看看这个添加进去的函数是不是理论上考虑的那样指向新的函数IP呢?通过源码一看果不其然。
图7 kpatch_ftrace_handler()源码
整个函数看起来很普通,但是它却是kpatch的精髓所在,因为奇迹就发生在这里,通过更新regs->ip来告诉ftrace,当旧函数被调用的时候,就会跳转到新函数去执行。此函数其实是一个跳板,它会被插入到旧函数的入口ftrace hook点当中,当我们调用旧函数的时候,这时kpatch_ftrace_handler就会被调用起来干活了。
所以,通过对kpatch的原理分析,可以发现,它其实就是使用ftrace进行打补丁操作,主要完成如下事宜:
使用HOOK链接到目标函数寄存器;
修改regs->ip指针,来指向新函数。
三、应用实践
BC-Linux中已经对Kpatch内核热补丁源码进行了定制化开发,不过此处主要是实践它是否如原理所讲的那样。对于测试补丁,这里使用社区的例子,分别修改/fs/proc/meminfo.c下meminfo_proc_show()函数中的文件,使得系统打印/proc/meminfo里的关键字VmallocChunk、MemTotal为全大写。
图8 生成两个补丁示例3.1 编译并安装补丁
通过kpatch-build命令可以对源码包、源代码、vmlinux等场景下的patch进行编译,以生成patch.ko模块。通过kpatch load、kpatch unload、kpatch list、kpatch install、kpatch uninstall等命令对patch.ko模块进行安装、卸载、查看等操作。
图9 编译与安装补丁
3.2 同一位置安装多个补丁模块时,结果如何?
图10 同一函数安装两个补丁模块当在同一位置安装多个补丁模块时,需在前一个补丁源码基础之上生成新的补丁,并且安装多个热补丁后,只有最后有个才能生效。
图11 卸载补丁模块并查看可是当卸载热补丁时,只能以安装顺序反过来的方式进行卸载,否则会出现卸载失败,并且最后一个卸载后,前一个的热补丁模块会生效。
3.3 nop、ftrace_regs_caller()/ftrace_caller()
是否真存在?
图12 crash状态查看通过手动Kdump的方式生成安装两个补丁模块后的vmcore,然后通过Crash的方式打开,函数的开头会有5个字节的nop。当打上补丁后,旧函数的开头已经被替换,利用“call 函数”命令方式调用ftrace_regs_caller(),最终引导出新函数。
网友评论