QCon北京「鸿蒙专场」火热来袭!即刻报名,与创新同行~ 了解详情
写点什么

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

  • 2019-11-12
  • 本文字数:2507 字

    阅读完需:约 8 分钟

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

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


多年来我们使用内核热补丁技术避免了系统重启导致的业务中断、保证了操作系统的可用性。属于核心业务组件的应用程序,尤其是单点的虚拟化组件和有状态的应用程序,同样面对高可用的挑战,每次重启都会导致服务受损。


然而业界并没有成熟可靠的应用程序热补丁方案可以参考,原因在于应用程序热补丁比内核热补丁更加困难和复杂。比如内核对外提供完整的模块加载功能,可以直接加载内核模块形式的热补丁。


而应用程序需要通过外部程序通过一系列对其内存和寄存器的复杂操作来注入动态链接库形式的热补丁;应用程序包含多线程;内核在编译时会被限定使用特定的编译方式,而应用程序的编译方式则更加宽泛;内核的二进制相对简单,而应用程序二进制因为需要链接到多种的动态链接库,本身的结构会更复杂。


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

原理

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


下面我们分别介绍热补丁本身和热补丁加载程序,热补丁本身是因 patch 而异的,加载程序是通用的。假设我们有热补丁加载程序 Loader、目标进程 T、热补丁 patch.so,目标程序的 func 函数替换为 func_v2。

热补丁

  • 编写热补丁源码,编译成动态链接库的格式的热补丁 patch.so,patch.so 中包含 func 和 func_v2 的信息。

  • 热补丁 patch.so 在被加载程序 Loader 加载到目标进程 T 地址空间的过程中,通过 dlsym 调用找到 func 的地址,并将 func 的入口指令改为可写,同时改变为跳转到 func_v2。

  • 至此,所有对 func 的调用都会被重定向到 func_v2,func_v2 执行完毕后返回,程序继续运行。

  • 如图所示:


热补丁加载程序

  • 加载程序 Loader 找到目标进程 T 的 dlopen 函数入口地址。

  • Loader 通过 ptrace 依附到目标进程 T,Loader 将热补丁的名字放入放入目标进程 T 的堆栈,将 IP 寄存器设置为 dlopen 函数的地址。

  • Loader 使目标进程 T 继续运行。因为 IP 寄存器已经设置为 dlopen 函数的入口,目标进程 T 会调用 dlopen 把热补丁加载到 T 的地址空间中。

  • 如图所示:



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

实现

热补丁

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


热补丁加载程序

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


  • dlopen 函数有 libdl 提供,并不是所有的程序都加载 libdl,幸运的是,libc 中提供了同样功能的函数 __libc_dlopen_mode,并且接受的参数和 dlopen 相同。除非特殊情况,所有程序都会加载 libc。所以我们需要找到 __libc_dlopen_mode 在目标进程 T 地址空间中的函数入口地址。

  • 我们知道,不同进程中 libc 会被加载到不同的基地址,但是 libc 中函数的地址相对基地址的偏移是不变的。

  • 通过 Loader 和目标进程 T 的 /proc/pid/maps,我们可以得到 libc 在 Loader 和目标进程 T 中加载的基地址。通过 Loader 运行 dlsym,我们可以得到 Loader 中的__libc_dlopen_mode 的地址。这样我们可以得到目标进程 T 中__libc_dlopen_mode 的地址(Loader_dlopen - Loader_libc + T_libc)。



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



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



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



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



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

验证

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



target 程序由 target 本身和 libold.so 组成,分别代码如下:



编译如下:



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



编译:



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



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


总结

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


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


  • 手写热补丁代码门槛较高,特别是被修复函数的依赖函数链较长时手写热补丁很容易出错;

  • 无法修复局部函数和局部变量(只能修复全局可见的函数和变量)。


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


本文转载自公众号 UCloud 技术(ID:ucloud_tech)。


原文链接:


https://mp.weixin.qq.com/s/BJhpWihFh3UcxMrvaZaOnw


2019-11-12 16:184017

评论

发布
暂无评论
发现更多内容

这几款小程序插件可以让效率翻倍

Geek_99967b

小程序

解读Vue3模板编译优化

yyds2026

Vue

展示出你的创意,知识库搭建也可以这样玩!

Baklib

外包学生管理系统

早安

架构

小程序容器技术,或许是提升智能设备安全等级的又一解决方案

Speedoooo

浏览器安全 小程序容器

如何打造优秀的客户体验?

Baklib

客户体验 客户体验管理

Python基础(十四) | Python之禅与时间复杂度分析

timerring

Python 时间复杂度 10月月更 python之禅

RocketMQ Streams在云安全及 IoT 场景下的大规模最佳实践

阿里巴巴云原生

阿里云 云原生 Apache RocketMQ

企业如何低成本快速搭建团队知识库,实现企业知识管理?

Baklib

KunlunBase功能体验范例

KunlunBase昆仑数据库

MySQL 数据库

英国国民信托 CIO:如何讲好可持续发展故事

雨果

CIO

软件测试 | 测试开发 | 测试过程中遇到的那些奇葩bug

测吧(北京)科技有限公司

测试

vue的几个提效技巧

yyds2026

Vue

企业级低代码开发平台有哪些?

优秀

企业级低代码平台 企业级低代码

Flowable 设置任务处理人的四种方式

江南一点雨

Java springboot flowable JavaEE

【一Go到底】第十天---位运算and移位运算符

指剑

Go golang 10月月更

如何搭建知识库网页?

Baklib

如何制作企业在线产品手册?这里有一些简单的方法!

Baklib

强引用、软引用、弱引用、幻象引用的区别

zarmnosaj

10月月更

【指针】有哪些类型?

Geek_65222d

10月月更

epoll的实现原理

C++后台开发

数据结构 后台开发 linux开发 epoll C++开发

量化自动套利分红机器人系统开发(成熟技术)

开发微hkkf5566

详解webpack构建优化

Geek_02d948

webpack

Apache SeaTunnel(Incubating) 2.2.0-beta 版本发布!API 重构,连接器与引擎解偶

Apache SeaTunnel

数据同步 Seatunnel 版本发布 数据集成平台 新版本/特性发布

全彩LED显示屏在单位形象的作用

Dylan

LED显示屏 全彩LED显示屏 led显示屏厂家

5分钟带您了解DSL、以太网线缆和光纤之间的区别,值得收藏!

wljslmz

dsl 光纤 以太网 10月月更

聊聊Vuex原理

yyds2026

Vue

Apache RocketMQ 5.0 在Stream场景的存储增强

阿里巴巴云原生

阿里云 云原生 Apache RocketMQ

实验精神终将胜利:量子纠缠的祛魅七十年

脑极体

我奋斗了 18 年才和你坐在一起喝咖啡

宇宙之一粟

顶级理解!阿里这份Github星标63.7K的Redis高级笔记简直不要太细

程序知音

Java 数据库 redis 架构 后端技术

应用程序热补丁(一):几行代码构造免重启修复补丁_文化 & 方法_王超_InfoQ精选文章