作者简介:王超,UCloud内核团队

前言

热补丁是一种在程序运行时动态修复内存中代码bug的技术。在UCloud,我们使用内核热补丁和应用程序热补丁(也就是进程热补丁)来在线修复核心业务的缺陷和安全漏洞。

多年来我们使用内核热补丁技术避免了系统重启导致的业务中断、保证了操作系统的可用性。属于核心业务组件的应用程序,尤其是单点的虚拟化组件和有状态的应用程序,同样面对高可用的挑战,每次重启都会导致服务受损。然而业界并没有成熟可靠的应用程序热补丁方案可以参考,原因在于应用程序热补丁比内核热补丁更加困难和复杂,比如内核对外提供完整的模块加载功能,可以直接加载内核模块形式的热补丁,而应用程序需要通过外部程序通过一系列对其内存和寄存器的复杂操作来注入动态链接库形式的热补丁;应用程序包含多线程;内核在编译时会被限定使用特定的编译方式,而应用程序的编译方式则更加宽泛;内核的二进制相对简单,而应用程序二进制因为需要链接到多种的动态链接库,本身的结构会更复杂。

经过大量的研究和实践,我们针对应用程序如何免重启修复BUG,自研了一套应用程序热补丁技术而且在UCloud内部已经经过数十万台次修复验证。后面通过一系列文章分享其技术实现。本文先介绍一种简单实用的应用程序热补丁技术,不少场景下采用该方法编写几行代码即可免修复应用程序BUG。

原理

一般来说,应用程序热补丁的流程是,首先通过编译器将热补丁源码制作成可加载的动态链接库,然后通过加载程序将热补丁加载到目标进程的地址空间,最后在进行一致性模型检查确认安全的情况下,把原始代码替换成新的代码,完成在线修复的过程。

下面我们分别介绍热补丁本身和热补丁加载程序,热补丁本身是因patch而异的,加载程序是通用的。

假设我们有热补丁加载程序Loader、目标进程T、热补丁patch.so,目标程序的func函数替换为func_v2。

热补丁:

  1. 编写热补丁源码,编译成动态链接库的格式的热补丁patch.so,patch.so中包含func和func_v2的信息。
  2. 热补丁patch.so在被加载程序Loader加载到目标进程T地址空间的过程中,通过dlsym调用找到func的地址,并将func的入口指令改为可写,同时改变为跳转到func_v2。
  3. 至此,所有对func的调用都会被重定向到func_v2,func_v2执行完毕后返回,程序继续运行。
  4. 如图所示:


热补丁加载程序:

  1. 加载程序Loader找到目标进程T的dlopen函数入口地址。
  2. Loader通过ptrace依附到目标进程T,Loader将热补丁的名字放入放入目标进程T的堆栈,将IP寄存器设置为dlopen函数的地址。
  3. Loader使目标进程T继续运行。因为IP寄存器已经设置为dlopen函数的入口,目标进程T会调用dlopen把热补丁加载到T的地址空间中。
  4. 如图所示:

了解原理之后,我们一步步实现一种简单的基于x86_64的热补丁。
(对于需要制作热补丁的同学,只需自己编写patch.so,而Loader是通用的。patch.so编写可以参考下面的例子,往往只需几行代码做相应替换。)

实现

热补丁

1.目标进程T执行dlopen的过程中,通过预先在热补丁(动态链接库)中写入的constructor函数,在加载过程中函数func_v1替换函数func。

static void __attribute__((constructor)) init(void)
{int numpages;void *old_func_entry, *new_func_entry;old_func_entry = dlsym(NULL, "func");new_func_entry = dlsym(NULL, "func_v2");#define PAGE_SHIFT              12
#define PAGE_SIZE               (1UL << PAGE_SHIFT)
#define PAGE_MASK               (~(PAGE_SIZE-1))numpages = (PAGE_SIZE - (old_func_entry & ~PAGE_MASK) >= size) ? 1 : 2;mprotect((void *)(old_func_entry & PAGE_MASK), numpages * PAGE_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC);/** Translate the following instructions * * mov $new_func_entry, %rax * jmp %rax * * into machine code * * 48 b8 xx xx xx xx xx xx xx xx * ff e0 */memset(old_func_entry, 0x48, 1);memset(old_func_entry + 1, 0xb8, 1); memcpy(old_func_entry + 2, &new_func_entry, 8); memset(old_func_entry + 10, 0xff, 1);memset(old_func_entry + 11, 0xe0, 1);
}

热补丁加载程序

1.Loader得到目标进程T地址空间中dlopen入口地址

1.1. dlopen函数有libdl提供,并不是所有的程序都加载libdl,幸运的是,libc中提供了同样功能的函数__libc_dlopen_mode,并且接受的参数和dlopen相同。除非特殊情况,所有程序都会加载libc。所以我们需要找到__libc_dlopen_mode在目标进程T地址空间中的函数入口地址。
1.2. 我们知道,不同进程中libc会被加载到不同的基地址,但是libc中函数的地址相对基地址的偏移是不变的。
1.3. 通过Loader和目标进程T的/proc/pid/maps,我们可以得到libc在Loader和目标进程T中加载的基地址。通过Loader运行dlsym,我们可以得到Loader中的__libc_dlopen_mode的地址。这样我们可以得到目标进程T中__libc_dlopen_mode的地址(Loader_dlopen - Loader_libc + T_libc)。

/* Take a hint and find start addr in /proc/pid/maps */
static unsigned long find_lib_base(pid_t pid, char *so_hint)
{FILE *fp;char maps[4096], mapbuf[4096], perms[32], libpath[4096];char *libname;unsigned long start, end, file_offset, inode, dev_major, dev_minor;sprintf(maps, "/proc/%d/maps", pid);fp = fopen(maps, "rb");if (!fp) {fprintf(stderr, "Failed to open %s: %s\n", maps, strerror(errno));return 0;}while (fgets(mapbuf, sizeof(mapbuf), fp)) {sscanf(mapbuf, "%lx-%lx %s %lx %lx:%lx %lu %s", &start,&end, perms, &file_offset, &dev_major, &dev_minor, &inode, libpath);libname = strrchr(libpath, '/');if (libname)libname++;elsecontinue;if (!strncmp(perms, "r-xp", 4) && strstr(libname, so_hint)) {fclose(fp);return start;}}fclose(fp);return 0;
}loader_libc = find_lib_base(getpid(), "libc-c");
T_libc = find_lib_base(T_pid, "libc-");
Loader_dlopen = (unsigned long)dlsym(NULL, “__libc_dlopen_mode”);
T_dlopen = T_libc + (Loader_dlopen - Loader_libc);

2.Loader对目标进程T使用ptrace attach,并保存T此时的寄存器信息。

static int ptrace_attach(pid_t pid)
{int status;if (ptrace(PTRACE_ATTACH, pid, NULL, NULL)) {fprintf(stderr, "Failed to ptrace_attach: %s\n", strerror(errno));return 1;}if (waitpid(pid, &status, __WALL) < 0) {fprintf(stderr, "Failed to wait for PID %d, %s\n", pid, strerror(errno));return 1;}return 0;
}static int ptrace_call(pid_t pid, unsigned long func_addr, unsigned long arg1, unsigned long arg2, unsigned long *func_ret)
{
…memset(&saved_regs, 0, sizeof(struct user_regs_struct));ptrace_getregs(pid, &saved_regs);
…
}

3.将目标进程T的%RIP指向dlopen,热补丁的名字的字符串放入堆栈,字符串的地址写入%rdi,RTLD_NOW的值写入%rsi作为dlopen的flag。同时把dlopen返回地址设置为非法地址0x0(把0x0压入栈中),这样Loader可以捕获目标进程T产生的SIGSEGV信号进而重新获得T的控制权。

unsigned long invalid = 0x0;
regs.rsp -= sizeof(invalid);
ptrace_poketext(pid, regs.rsp, ((void *)&invalid), sizeof(invalid));
ptrace_poketext(pid, regs.rsp + 512, filename, strlen(filename) + 1);
regs.rip = dlopen_addr;
regs.rdi = regs.rsp + 512;
regs.rsi = RTLD_NOW;
ptrace_setregs(pid, &regs);

4.Loader使目标进程T继续运行。当T执行完dlopen之后,T产生的SIGSEGV信号被Loader捕获,Loader重新获得T进程的控制权。

static int ptrace_cont(pid_t pid)
{int status;if (ptrace(PTRACE_CONT, pid, NULL, 0)) {fprintf(stderr, "Failed to ptrace_cont: %s\n", strerror(errno));return 1;}if (waitpid(pid, &status, __WALL) < 0) {fprintf(stderr, "Failed to wait for PID %d, %s\n", pid, strerror(errno));return 1;}return 0;
}

5.Loader通过读取目标进程T此时的%rax寄存器得到dlopen的返回值,恢复T最开始的执行状态,最后释放对T的控制

ptrace_getregs(pid, &regs);
dlopen_ret = regs.rax;
ptrace_setregs(pid, &saved_regs);
ptrace_detach(pid);

至此对目标进程T的热补丁就完成了。下面我们看一个例子。

验证

假设我们运行target程序,每隔一秒打印Hello一次:

# ./target
Hello
Hello
…target程序由target本身和libold.so组成,分别代码如下:/* target.c */
#include <unistd.h>
#include "old.h"int main() {for (;;) {print();sleep(1);}
}/* old.c */
#include <stdio.h>void print(void)
{printf("Hello\n");
}

编译:gcc -fPIC –shared old.c -o libold.so
gcc target.c ./libold.so -o target

我们想要修改print函数,变成打印“Goodbye”。我们需要编写热补丁new.c,并添加新函数和constructor:

/* new.c */
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <dlfcn.h> void print_v2(void)
{printf("Goodbye\n");
}static void __attribute__((constructor)) init(void)
{ int numpages;void *old_func_entry, *new_func_entry;old_func_entry = dlsym(NULL, print);new_func_entry = dlsym(NULL, print_v2);#define PAGE_SHIFT              12
#define PAGE_SIZE               (1UL << PAGE_SHIFT)
#define PAGE_MASK               (~(PAGE_SIZE-1))numpages = (PAGE_SIZE - (old_func_entry & ~PAGE_MASK) >= size) ? 1 : 2;mprotect((void *)(old_func_entry & PAGE_MASK), numpages * PAGE_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC);memset(old_func_entry, 0x48, 1);memset(old_func_entry + 1, 0xb8, 1); memcpy(old_func_entry + 2, &new_func_entry, 8); memset(old_func_entry + 10, 0xff, 1);memset(old_func_entry + 11, 0xe0, 1);
} 

编译:gcc -fPIC –shared new.c -ldl -o libnew.so

然后通过加载程序对target进程打入热补丁libnew.so,最后我们对target程序打入这个热补丁,观察变化:


# ./target
Hello
Hello
Goodbye
Goodbye
…

我们发现热补丁确实改变了print函数,最后通过gdb进一步确认,可以看出print函数的入口被修改成48 b8 dc b6 15 a9 c1 7f 00 00 ff e0,与我们的预期相符:

(gdb) disas /r print
Dump of assembler code for function print:0x00007fc1a98f456c <+0>:     48 b8 dc b6 15 a9 c1 7f 00 00   movabs $0x7fc1a915b6dc,%rax0x00007fc1a98f4576 <+10>:    ff e0   jmpq   *%rax # 这里print在入口处跳转到0x7fc1a915b6dc这个地址
…
(gdb) info symbol 0x7fc1a915b6dc
print_v2 in section .text of /root/process-hotupgrade/test/libnew.so # 0x7f2ea417971c这个地址就是print_v2函数的地址

总结

我们介绍了应用程序热补丁的基本原理,实践了一个应用程序热补丁demo。此类热补丁适用于动态替换共享链接库中的可见函数,可以修复例如glibc “GHOST漏洞”(CVE-2015-0235)等等,在UCloud我们利用热补丁修复了若干缺陷,在用户没有感知的情况下把bug快速及时的修复。这些热补丁修复程序里,绝大多数代码是通用的,只需少数几行做特殊替换。

上文介绍的热补丁技术对于适用的场景非常理想,简单可靠,但存在几个缺点:

  • 手写热补丁代码门槛较高,特别是被修复函数的依赖函数链较长时手写热补丁很容易出错
  • 无法修复局部函数和局部变量(只能修复全局可见的函数和变量)

后面的文章我们会介绍如一种更加先进的应用程序热补丁技术。

应用程序热补丁(一): 几行代码构造免重启修复补丁相关推荐

  1. 老人寻求到一名程序员,用2W行代码给自己打造了一幅肖像画

    今天翻墙看了下国外的论坛,看到了一位版主给一位老人描绘肖像画的文章,不得不说这位大佬是真的厉害,近20000行代码,而且还画的很像,像小编我这种手残党,用笔也不能画出来,不得不服,今天就给头条的小伙伴 ...

  2. python代码300行程序_python小工具,15行代码秒出工资条

    公司工资条经常使用Excel制作,但是每个月都要做一遍,能不能用python写个程序自动化完成这想工作?当然可以,而且只是分分钟的事! 先来看看原始数据是什么样子: 最后做成的效果: 使用Excel每 ...

  3. python黑色和浅黑色的代码_黑色格式化程序-忽略特定的多行代码 - python

    我想忽略black python formatter的特定多行代码.特别是,它用于np.array或格式化时难看的矩阵构造.以下是示例. np.array( [ [1, 0, 0, 0], [0, - ...

  4. 25行AS3代码编程大赛的第一名!25行代码构造的AS3游戏

    叫做Marius Heil的AS开发者使用AS3构造了一个25行代码的AS3游戏,并且获得了25行AS3代码编程大赛的第一名! /** * 25-Line ActionScript Contest E ...

  5. 没想到,这个程序员只用了20行代码就拿了冠军

    你知道的越多,你不知道的越多 点赞再看,养成习惯 GitHub上已经开源https://github.com/JavaFamily,有一线大厂面试点脑图,欢迎Star和完善 这期不算面试的知识点,来只 ...

  6. c语言求婚代码大全,程序员七夕用40行代码向女友求婚成功,网友:求源码

    今天刚来上班道学数里屏.中近,期据面蔽最,近,期据面逛下论坛,看到了一位浪漫程序员用代码向女朋友求婚,小编我也运行了下他分享的代码,挺有意思的今天就分享给大伙了,源码文末有领取地址.谁说程序员只是个敲 ...

  7. 程序员日均写7行代码被开除,公司:正常员工每天200行

  8. LibcarePlus用户态程序热补丁

    LibcarePlus https://gitee.com/openeuler/docs/edit/stable2-20.03_LTS_SP1/docs/zh/docs/Virtualization/ ...

  9. 一天写多少行代码才算是好程序员?

    点击查看全文 当今时代很多吃瓜群众对程序员的印象都是 而程序员的真实情况却是这样:每天大部分时间都在改代码,写代码,看代码才是我们真实的工作日常,到底每天要写多少行才是好程序员呢? 为了探讨这个问题首 ...

最新文章

  1. 技术网站/博客网址收藏
  2. 小程序判断数组的index是否为空_微信小程序之购物车功能(仅学习)
  3. 从安全测试开始:与杰夫•佩恩的一场面谈(译)
  4. 图像处理——Edge Boxes边缘检测
  5. Asp.net HttpClient Proxy(Fiddler)
  6. C# winform WebBrowser怎么获取js中的变量的值?怎么触发js的事件?
  7. 微信电脑客户端登陆_电脑端的微信只能开一个?简单操作就能随意开
  8. magento如何在首页显示产品
  9. 浙大PAT的大量感悟
  10. 配置登入是显示服务器信息
  11. SqlServer存储过程调用接口
  12. Python MySQL 教程
  13. uniapp引入高德地图sdk经纬度解析诚地址名称
  14. ramda 函数 String
  15. java烟花代码详细步骤,一文说清!
  16. jQuery菜鸟教程01
  17. 一种基于机器学习的电影推荐系统设计
  18. 沐风老师ATilesPro for 3dMax屋顶设计插件使用教程
  19. 在 Notepad++ 运行 Closure Linter 来校验JS代码
  20. Swift 调用C++代码

热门文章

  1. 【C语言】案例三十三 【二维数组】神奇魔方阵
  2. 【华为OD机试 2023最新 】组装新的数组(C语言题解 100%)
  3. 压电陶瓷超声波换能器设计
  4. moran指数 r语言_使用R进行空间自相关检验
  5. 苹果手机计算机的使用记录,苹果iPhone手机求一款记录日常工作内容的便签app
  6. NB-IOT门磁的应用说明
  7. 我们的档案该怎么处理?
  8. 计算机综合症要拍片,害怕去医院拍片,辐射太可怕?关于影像学检查的真相在这里...
  9. Failure downloading binaries (curl RC=56). Please try again and .....问题解决
  10. day04——判断和循环