From 30b98230dc3bb3b289285a981628e5613b88d37d Mon Sep 17 00:00:00 2001 From: lijiajie128 Date: Tue, 15 Dec 2020 17:05:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0LibcarePlus=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...77\347\224\250\346\214\207\345\215\227.md" | 552 ++++++++++++++++++ 1 file changed, 552 insertions(+) diff --git "a/docs/zh/docs/Virtualization/\345\267\245\345\205\267\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/docs/zh/docs/Virtualization/\345\267\245\345\205\267\344\275\277\347\224\250\346\214\207\345\215\227.md" index 5395459b3..dc8865fd8 100644 --- "a/docs/zh/docs/Virtualization/\345\267\245\345\205\267\344\275\277\347\224\250\346\214\207\345\215\227.md" +++ "b/docs/zh/docs/Virtualization/\345\267\245\345\205\267\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -1,6 +1,7 @@ # 工具使用指南 - [vmtop使用指南](#vmtop使用指南) +- [LibcarePlus使用指南](#LibcarePlus使用指南) ## vmtop使用指南 @@ -138,3 +139,554 @@ Domains: 1 running |_ worker 1794 0.0 0 0 0 0 0 0 0 0 0 0 S 126 ``` %ST、%GUE、%HYP将不会出现在显示界面上。 + + + +## LibcarePlus使用指南 + +### 概述 +LibcarePlus是一个用户态进程热补丁框架。LibcarePlus可以在不重启进程的情况下实现对在Linux系统上运行的目标进程进行热补丁操作。热补丁可以应用于CVE漏洞修复,也可以应用于避免应用服务中断的紧急bug修复。 + +### 约束限制 +- 当前只支持x86体系结构。 +- 只支持C语言编译得到的ELF文件,不支持其它语言。 +- 代码文件名称必须符合C语言标示符命名规范:由字母(A-Z,a-z)、数字 (0-9)、下划线“_”组成,并且首字符不能是数字,但可以是字母或者下划线,不能包含“-”、“$”等特殊符号。 +- 被打补丁的目标函数的出入参不可以增删。 +- 不支持对死循环、不退出的函数打补丁,补丁函数执行不到。 +- 不支持数据结构成员变化,可以制作补丁的时候通过新增数据结构来规避。 +- 不支持初始化函数打补丁。 +- 不支持头文件的修改。 +- 不支持替换全局变量。 +- 不支持inline函数。 +- 最多支持9999个补丁,即补丁号的范围为[1,9999]。 +- 不支持NMI中断处理函数。 +- 不支持函数符号表不存在的静态函数。 +- 动态库热补丁只支持对调用该动态库的进程打补丁。 +- 不支持动态库内静态函数和静态变量打补丁。 + +### LibcarePlus安装指南 +#### 前言 +LibcarePlus可以在任何支持安装 **libunwind**、 **elfutils** 以及 **binutils** 的Linux发行版系统上运行。 + +但目前 LibcarePlus 只在openEuler 20.03 LTS SP1版本上测试过。 + +#### 软件依赖 +在openEuler上,可以采用以下指令安装LibcarePlus所需要的依赖软件. + +``` shell +$ sudo yum install -y binutils elfutils elfutils-libelf-devel libunwind-devel +``` + +#### 安装软件 + +安装LibcarePlus有两种方式:第一种方式是**编译安装**;第二种方式是**yum源安装**。接下来针对两种安装方式,分别给出具体的安装过程。 + +##### 编译安装 + +为了编译LibcarePlus,需要先克隆代码工程,然后执行编译命令,具体命令如下: + +``` shell +$ git clone https://gitee.com/openeuler/libcareplus.git +$ cd libcareplus +$ make -C src +``` + +以上命令已经编译得到LibcarePlus的所有可执行程序,继续执行以下命令,LibcarePlus会将所有可执行程序放到 **/usr/local/bin** 目录之下,至此LibcarePlus的编译安装已完成。 + +```shell +$ cd libcareplus/src +$ make install +``` + +##### yum源安装 + +yum源安装LibcarePlus,直接运行以下命令: + +```shell +$ yum install LibcarePlus -y +``` + +至此LibcarePlus的yum源安装已完成,可以通过以下命令查看安装是否成功: + +``` shell +$ libcare-ctl -help +usage: libcare-ctl [options] [args] + +Options: + -v - verbose mode + -h - this message + +Commands: + patch - apply patch to a user-space process + unpatch- unapply patch from a user-space process + info - show info on applied patches + server - listen on a unix socket for commands +``` + +### LibcarePlus热补丁制作指南 +#### 前言 +热补丁制作所需的最关键的可执行程序是**kpatch_gensrc**。 +**kpatch_gensrc**会对比原汇编文件与补丁汇编文件,将其中的不同之处提取出来,加入到以.kpatch.为前缀的多个section中。 +这些以.kpatch为前缀的section加上原文件的所有的section,形成一个新的汇编文件作为**kpatch_gensrc**的输出。 + +LibcarePlus支持两种热补丁制作方式,分别是**手动制作**和**脚本制作**。接下来针对两种制作方式,分别给出具体的制作过程。 + +#### 手动制作 +接下来的过程默认LibcarePlus已经被安装成功,具体安装过程参考《LibcarePlus安装指南》。 + +假设现在有两个C语言所写的文件,分别是原文件 foo.c 和补丁文件 bar.c 。 + +
+点击展开 foo.c +

+ +``` c +// foo.c +#include +#include + +void print_hello(void) +{ + printf("Hello world!\n"); +} + +int main(void) +{ + while (1) { + print_hello(); + sleep(1); + } +} +``` + +

+
+ +
+点击展开 bar.c +

+ +``` c +// bar.c +#include +#include + +void print_hello(void) +{ + printf("Hello world %s!\n", "being patched"); +} + +int main(void) +{ + while (1) { + print_hello(); + sleep(1); + } +} +``` + +

+
+ +首先需要通过编译得到两者分别对应的汇编文件 foo.s 和 bar.s,具体命令如下: + +``` shell +$ gcc -S foo.c +$ gcc -S bar.c +$ ls +bar.c bar.s foo.c foo.s +``` + +接下来使用**kpatch_gensrc**来对比foo.s和bar.s,具体命令如下: + +``` shell +$ sed -i 's/bar.c/foo.c/' bar.s +$ kpatch_gensrc --os=rhel6 -i foo.s -i bar.s -o foobar.s --force-global +``` + +由于**kpatch_gensrc**的对比是默认对同一个C语言源文件进行的,所以首先要利用sed命令修改bar.s内源文件的名字为foo.c。随后调用**kpatch_gensrc**,指定输入文件为foo.s与bar.s,输出文件为foobar.s。foobar.s的具体内容如下所示: + +
+点击展开 foobar.s +

+ +```x86asm + .file "foo.c" +#---------- var --------- + .section .rodata +.LC0: + .string "Hello world!" +#---------- func --------- + .globl print_hello + .text + .globl print_hello + .type print_hello, @function +print_hello: +.LFB0: + .cfi_startproc + pushq %rbp + .cfi_def_cfa_offset 16 + .cfi_offset 6, -16 + movq %rsp, %rbp + .cfi_def_cfa_register 6 + movl $.LC0, %edi + call puts + popq %rbp + .cfi_def_cfa 7, 8 + ret + .cfi_endproc +.LFE0: + .size print_hello, .-print_hello +print_hello.Lfe: +#---------- kpatch begin --------- + .pushsection .kpatch.text,"ax",@progbits + .globl print_hello.kpatch + .globl print_hello.kpatch + .type print_hello.kpatch, @function +print_hello.kpatch: +.LFB0.kpatch: + .cfi_startproc + pushq %rbp + .cfi_def_cfa_offset 16 + .cfi_offset 6, -16 + movq %rsp, %rbp + .cfi_def_cfa_register 6 + movl $.LC0.kpatch, %esi + movl $.LC1.kpatch, %edi + movl $0, %eax + call printf + popq %rbp + .cfi_def_cfa 7, 8 + ret + .cfi_endproc +.LFE0.kpatch: + .size print_hello.kpatch, .-print_hello.kpatch +print_hello.kpatch_end: + .popsection + + .pushsection .kpatch.strtab,"a",@progbits +kpatch_strtab1: + .string "print_hello.kpatch" + .popsection + .pushsection .kpatch.info,"a",@progbits +print_hello.Lpi: + .quad print_hello + .quad print_hello.kpatch + .long print_hello.Lfe - print_hello + .long print_hello.kpatch_end - print_hello.kpatch + .quad kpatch_strtab1 + .quad 0 + .long 0 + .byte 0, 0, 0, 0 + .popsection + +#---------- kpatch end ----------- +#---------- func --------- + .globl main + .globl main + .type main, @function +main: +.LFB1: + .cfi_startproc + pushq %rbp + .cfi_def_cfa_offset 16 + .cfi_offset 6, -16 + movq %rsp, %rbp + .cfi_def_cfa_register 6 +.L3: + call print_hello + movl $1, %edi + movl $0, %eax + call sleep + jmp .L3 + .cfi_endproc +.LFE1: + .size main, .-main + .pushsection .kpatch.data,"aw",@progbits +.LC0.kpatch: + .string "being patched" + .popsection + .pushsection .kpatch.data,"aw",@progbits +.LC1.kpatch: + .string "Hello world %s!\n" + .popsection + .ident "GCC: (GNU) 4.8.5 20150623 (EulerOS 4.8.5-28)" + .section .note.GNU-stack,"",@progbits +``` + +

+
+ +可以发现,foobar.s内多了.kpatch.text 、.kpatch.info 、.kpatch.data 、 .kpatch.strtab 这四个section。而其余的汇编内容与foo.s依旧保护一致。接下来可以利用生成的汇编文件进行编译,得到可执行程序,具体命令如下: + +``` shell +$ gcc -o foo foo.s +$ gcc -o foobar foobar.s -Wl,-q +``` + +通过readelf命令可以发现:生成的foobar比foo多了6个以.kpatch为前缀的section,这多出来的6个section正是热补丁的来源。 + +``` x86asm +$ readelf -S foobar | grep -A 1 kpatch + [17] .kpatch.text PROGBITS 0000000000400652 00000652 + 000000000000001a 0000000000000000 AX 0 0 1 + [18] .rela.kpatch.text RELA 0000000000000000 00001d48 + 0000000000000048 0000000000000018 I 40 17 8 +-- + [21] .kpatch.strtab PROGBITS 0000000000400695 00000695 + 0000000000000013 0000000000000000 A 0 0 1 + [22] .kpatch.info PROGBITS 00000000004006a8 000006a8 + 0000000000000030 0000000000000000 A 0 0 1 + [23] .rela.kpatch.info RELA 0000000000000000 00001d90 + 0000000000000048 0000000000000018 I 40 22 8 +-- + [37] .kpatch.data PROGBITS 0000000000601040 00001040 + 000000000000001f 0000000000000000 WA 0 0 1 +``` + +接下来需要利用**kpatch_strip**来将这些section剥离出来,具体命令如下所示: + +``` shell +$ kpatch_strip --strip foobar foobar.stripped +$ kpatch_strip --rel-fixup foo foobar.stripped +$ strip --strip-unneeded foobar.stripped +$ kpatch_strip --undo-link foo foobar.stripped +``` + +其中 --strip 的作用是去除foobar中对于补丁制作无用的section;--rel-fixup 的作用是修复补丁这些section对于其他section变量以及函数访问的寻址方式; strip --strip-unneeded 的作用是去除对于热补丁重定位操作无用的符号信息; --undo-link 的作用是将补丁内符号的地址从绝对地址更改为相对地址。 + +通过以上操作,已经得到了热补丁制作所需的主要内容,接下来需要使用**kpatch_make**。将原可执行文件的 **Build ID** 以及 **kpatch_strip**的输出文件**foobar.stripped** 作为参数传递给**kpatch_make**,最终生成热补丁文件,具体命令如下: + +``` shell +$ str=$(readelf -n foo | grep 'Build ID') +$ substr=${str##* } +$ kpatch_make -b $substr foobar.stripped -o foo.kpatch +$ ls +bar.c bar.s foo foobar foobar.s foobar.stripped foo.c foo.kpatch foo.s +``` + +至此,就得到了最终的热补丁文件foo.kpatch。手动制作热补丁的过程繁琐,对于代码量较大的工程,例如QEMU,手动制作热补丁极其困难,因此接下来介绍如何利用LibcarePlus自带的脚本来一键式地生成热补丁文件。 + +#### 脚本制作 +接下来讲解如何利用LibcarePlus自带的**libcare-patch-make**脚本实现热补丁文件的制作,依旧以上文的foo.c和bar.c作为演示例子。 + +首先利用diff命令生成foo.c和bar.c的对比文件,命令如下所示: + +``` shell +$ diff -up foo.c bar.c > foo.patch +``` + +foo.patch文件内容如下所示: + +
+点击展开 foo.patch +

+ +``` diff +--- foo.c 2020-12-09 15:39:51.159632075 +0800 ++++ bar.c 2020-12-09 15:40:03.818632220 +0800 +@@ -1,10 +1,10 @@ +-// foo.c ++// bar.c + #include + #include + + void i_m_being_patched(void) + { +- printf("i'm unpatched!\n"); ++ printf("you patched my %s\n", "tralala"); + } + + int main(void) +``` + +

+
+ +接下来,需要编写一个编译foo.c的MakeFile文件,具体如下所示: + +
+点击展开 MakeFile +

+ +``` makefile +all: foo + +foo: foo.c + $(CC) -o $@ $< + +clean: + rm -f foo + +install: foo + mkdir $$DESTDIR || : + cp foo $$DESTDIR +``` + +

+
+ +编写好MakeFile之后,直接调用**libcare-patch-make**即可,若**libcare-patch-make**会询问选择哪个文件进行打补丁操作,输入原文件名即可,具体命令如下所示: + +``` shell +$ libcare-patch-make --clean foo.patch +rm -f foo +BUILDING ORIGINAL CODE +/usr/local/bin/libcare-cc -o foo foo.c +INSTALLING ORIGINAL OBJECTS INTO /libcareplus/test/lpmake +mkdir $DESTDIR || : +cp foo $DESTDIR +applying foo.patch... +can't find file to patch at input line 3 +Perhaps you used the wrong -p or --strip option? +The text leading up to this was: +-------------------------- +|--- foo.c 2020-12-10 09:43:04.445375845 +0800 +|+++ bar.c 2020-12-10 09:48:36.778379648 +0800 +-------------------------- +File to patch: foo.c +patching file foo.c +BUILDING PATCHED CODE +/usr/local/bin/libcare-cc -o foo foo.c +INSTALLING PATCHED OBJECTS INTO /libcareplus/test/.lpmaketmp/patched +mkdir $DESTDIR || : +cp foo $DESTDIR +MAKING PATCHES +Fixing up relocation printf@@GLIBC_2.2.5+fffffffffffffffc +Fixing up relocation print_hello+0 +patch for /libcareplus/test/lpmake/foo is in /libcareplus/test/patchroot/700297b7bc56a11e1d5a6fb564c2a5bc5b282082.kpatch +``` + +执行成功之后,输出显示:热补丁文件位于当前目录的**patchroot**目录下,可执行文件则在**lpmake**目录下。脚本生成的热补丁文件默认是采用BuildID作为热补丁文件的文件名。 + +#### 总结 +至此,手动制作热补丁以及脚本制作热补丁的具体流程讲解完毕,生成的热补丁文件即可用于之后的热补丁应用过程中。 + + + +### LibcarePlus热补丁应用指南 +#### 前期准备 +假设现在有两个C语言所写的文件,分别是原文件 foo.c 和补丁文件 bar.c 。 + +
+点击展开 foo.c +

+ +``` c +// foo.c +#include +#include + +void print_hello(void) +{ + printf("Hello world!\n"); +} + +int main(void) +{ + while (1) { + print_hello(); + sleep(1); + } +} +``` + +

+
+ +
+点击展开 bar.c +

+ +``` c +// bar.c +#include +#include + +void print_hello(void) +{ + printf("Hello world %s!\n", "being patched"); +} + +int main(void) +{ + while (1) { + print_hello(); + sleep(1); + } +} +``` + +

+
+ + +通过热补丁制作流程,得到了foo可执行程序以及foo.kpatch热补丁文件。测试目录下的结构大体如下所示: + +``` +.. (up a dir) +/libcareplus/test/ +▾ lpmake/ + foo* +▾ patchroot/ + 700297b7bc56a11e1d5a6fb564c2a5bc5b282082.kpatch + bar.c + foo* + foo.c + foo.patch + Makefile +``` + +#### 应用补丁具体流程 +首先在第一个shell窗口运行需要打补丁的可执行程序,如下所示: + +``` shell +$ ./lpmake/foo +Hello world! +Hello world! +Hello world! +``` + +随后在第二个shell窗口运行**libcare-ctl**,命令如下所示: + +``` shell +$ libcare-ctl -v patch -p $(pidof foo) ./patchroot/700297b7bc56a11e1d5a6fb564c2a5bc5b282082.kpatch +``` + +此时若热补丁应用成功,第二个shell窗口会有以下输出: + +``` shell +1 patch hunk(s) have been successfully applied to PID '10999' +``` + +而第一个shell窗口内运行的目标进程则会出现以下输出: + +``` shell +Hello world! +Hello world! +Hello world being patched! +Hello world being patched! +``` + +#### 卸载补丁具体流程 + +如果先要卸载补丁,可以在第二个shell窗口,执行以下命令: + +``` shell +$ libcare-ctl unpatch -p $(pidof foo) +``` + +此时若热补丁卸载成功,第二个shell窗口会有以下输出: + +``` shell +1 patch hunk(s) were successfully cancelled from PID '10999' +``` + +而第一个shell窗口内运行的目标进程则会出现以下输出: + +``` shell +Hello world being patched! +Hello world being patched! +Hello world! +Hello world! +``` -- Gitee