写点什么

应用程序热补丁(二): 自动生成热补丁

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

    阅读完需:约 15 分钟

应用程序热补丁(二): 自动生成热补丁

上篇文章中,我们介绍了应用程序热补丁技术的基本原理,同时实现了一个简单的热补丁。但是无法对本地函数打热补丁,同时手动编写热补丁比较麻烦、非常复杂且容易出错。


为了解决这些问题,本文将会介绍一种自动生成应用程序热补丁技术,可以生成应用程序和动态链接库中任意函数的热补丁。

1 自动生成热补丁综述

自动生成热补丁是利用热补丁生成工具,对现有的源代码和补丁文件进行处理,自动输出热补丁的技术。


我们知道,热补丁的基本原理是新函数替换旧函数,也就是完整的函数替换。补丁中可能包含对一个或多个函数的修改,这些被修改的函数都会被替换掉。上一篇文章介绍过,热补丁首先把新函数放入目标进程的内存中,然后修改旧函数的入口使之跳转到新函数。


自动生成热补丁中最主要的部分是自动生成新函数的二进制代码,也称为是替换代码。在生成替换代码时,主要由以下两部分组成:


1.自动生成替换代码;

2.解析替换代码中使用的符号。


接下来会对以上部分进行详细的介绍。在介绍之前,必须假设系统环境是 Linux X86/X86_64,应用程序是 C 语言编译链接的 ELF 格式可执行文件,并且拥有原始程序的源代码。

2 自动生成替换代码

  • 动机与挑战


为了生成替换代码,首先需要知道代码修复之后,哪些函数发生了改变,然后根据这些改变,生成替换代码。使用一种二进制比较的方法,通过比较原始程序和修复后程序的二进制,提取出生成替换代码所需的全部信息。我们选择对目标文件(object file)的级别进行前后比较。这样做的好处是显而易见的:


首先,任何源代码的改变都会在目标文件的二进制代码中显示出来。举个例子:头文件 .h 文件中函数的参数如果从 int 变成了 long long,调用这个函数的 .c 文件由于隐含的类型转换并不发生改变。如果前后代码对比发生在源代码级别,那么预处理之后也不能发现前后 .c 文件发生了改变。


其次,我们不需要处理语言级别的特性,比如 inline 关键字、隐含类型转换、宏等。这些语言相关的特性可能随着语言的发展会愈发复杂,并且我们也不希望热补丁局限于某种语言。C 语言、C++、汇编都是我们希望可以处理的语言。


最后,我们只需要关心代码的二进制表达。在目标文件的级别上,二进制代码的组成信息是最完整的,所以在此进行前后代码比较也是最合理的。


所以,生成替换代码的思路是——通过比较前后编译的目标文件,提取出差异部分,组成替换代码。


比较目标文件也面临很多挑战,主要在于如何提取出发生改变的函数。举个例子:由于目标文件中默认所有的函数代码都会放在 .text 段,同时 .text 段中的相对地址跳转都是相对于 .text 段的。也就是说,如果某个函数发生了改变,就算是一行代码的改变,后面的相对地址跳转也很可能会发生改变(由于符号位置发生了改变)。


  • 解决办法


我们对目标应用程序代码和修复后代码分别编译,逐一比较两次编译产生的若干个目标文件。如图所示:



  • 首先,我们编译原始源代码,保留所有中间过程中产生的目标文件;

  • 然后,我们对源代码打上修复补丁,再次编译,构建系统(make)一般只会编译改变的源文件,保留新生成的目标文件。如果没有构建系统或者构建系统不如期工作,可以保留所有的目标文件;

  • 最后,我们比较新生成的目标文件和对应的原始代码编译出来的目标文件,提取出差异部分,组成替换代码,生成热补丁。


在比较过程中,我们希望可以做到在函数级别上进行比较,这样就只需要提取出发生改变的函数;同时也希望生成的替换代码是与地址无关的,因为替换代码可能被加载到任意的内存地址。


GCC 编译器提供了 -ffunciton-sections 和 -fdata-section 的编译选项,作用是把函数和变量放入目标文件中独立的段(每个函数代码都由独立的段来表示)。这样编译出的函数代码都是与地址无关的、更加通用的二进制,从而提取到被加载到内存中任意位置运行的替换代码。


在对前后目标文件进行比较的时候,我们选择在 ELF 段的级别进行比较(也就是函数的级别,因为函数都在自己独立的段中),目标文件是 ELF 文件,遵守通用的标准。通过解析目标文件的 ELF Header,找到段的开头(Section Header)由此找到所有的段。这里我们建议使用一些 ELF 解析库,如 libelf、libbfd 等。


逐一比较前后目标文件中表示代码的段(段的内容就是函数的代码),找到发生改变的函数:


  • 首先,比较段的大小,如果大小不同,说明函数发生了改变;

  • 接着,对段的内容进行比较,如果某个字节不同,而且字节不属于重定向的一部分,说明函数发生了改变。如果是重定向,检查重定向计算之后的指令内容是否相同,如果不同,说明函数发生了改变(引用了与之前不同的函数或者变量);

  • 最后,如果段的大小和段的内容都没有改变,说明函数没有改变。


通过比较前后目标文件,我们可以找到发生了改变的段。然后经过一些特殊处理,将前后改变的函数信息(名称、大小、位置、包含的重定向信息等)和新目标文件内对应的段一起,组装成替换代码。


需要注意的是:由于宏和 inline 函数会在编译过程中被放置在其调用函数中,所有调用函数都改变,那么提取的是所有调用的函数。因此,如果补丁中修改的是宏或 inline 函数,我们无法做到只提取宏或者 inline 函数的差异。此时的替换代码还不能直接运行,因为替换代码中还可能包含对其他符号的引用,我们需要在运行替换代码之前解析这些符号。

3 解析替换代码中使用的符号

  • 动机与挑战


我们的替换代码中只包含改变的函数代码,这些函数中可能会引用其他没有改变的符号(函数或变量),所以我们需要根据符号引用的规则(一般为相对 PC 地址的相对地址),对引用符号手动重定向。


解析符号地址面临的挑战主要有两个:找到运行中程序的符号所在地址;如果存在两个或者以上相同名字的符号,找到正确符号。


  • 解决方法


符号的地址是在最终链接的时候决定的,如果可执行文件是 pie(position independent executable)或者是动态链接库,符号的地址是一个相对地址,是相对于可执行文件或者动态链接库的代码段在内存中地址的偏移,计算公式 Addr = Base + Offset。其他情况,符号的地址是一个绝对地址,无需计算。


同样名称的符号在可执行文件中可能出现多次(例如两个文件中定义了名字相同的 static 变量),我们需要从中找到正确的符号。在链接之后,可执行文件中的符号表会遵守一些固定的规则,相同源文件中符号会一起连续地记录,并且在记录的开头会有类型为 STT_FILE 的符号,名字为源文件的名字。


举个例子,我们在 1.c 和 2.c 中同时定义了 static int a,最终可执行文件中的符号表如下所示:



因此我们可以通过 file+sym 规则找到正确的符号位置。假设函数引用变量 a,并且函数在 2.c 源文件中,我们首先找到类型为 STT_FILE 的 2.c 这个符号,接着找到符号 a 就可以了。


我们知道符号的地址后就可以对替换代码中的引用进行手动重定向编写,将编译时符号引用的重定向转换为我们自己在运行时可以识别的重定向(self relocation):


  • 首先,根据重定向类型和计算公式,计算出引用的符号,记录符号的信息,其中符号的地址在运行时可以得到;


  • 然后,记录原有的重定向信息,其中重定向的地址在运行时可以得到;


  • 最后,在替换代码(热补丁)被加载到目标程序的内存中时,通过之前记录的重定向内容,修改替换代码符号引用位置的内容,内容由重定向的类型、重定向的地址、符号的地址计算得到。


举个例子:替换代码中包含函数 x,函数 x 会在地址 P 引用正在运行的程序中的函数 y。当替换代码被加载到程序的地址空间时,地址 P 可以被确定,函数 y 的符号地址 S 可以被确定,根据替换代码中记录的重定向信息(假设重定向类型是 R_X86_64_PC32,Addend 是 A),那么地址 P 的内容需要被修改为 S+A-P。这样当函数 x 执行时才能引用到函数 y 的位置。

4 生成替换代码、符号解析的 POC 验证

本章节利用 objdump 等工具,对生成替换代码中的关键步骤进行 POC 验证。假设我们有目标程序 T(修复后名为 T-patched),包含如下代码:



patch 文件如下:



分别将源代码和修复后的源代码编译,生成 T.o 和 T-patched.o。通过 objdump 我们可以发现 func 函数前后发生了改变,地址 0x4 的指令不同。(其他函数不变,省略)。如下所示:




所以提取出 T-patched.o 中的 .text.func 段就完成了替换代码的提取。因为 T-patched.o 中的 func 函数在 0xa 的位置上引用了 print 函数,我们需要对 print 符号进行解析,在替换代码(热补丁)被加载到目标进程内存时,对 0xa-0xd 的四个字节重定向。我们记录下这个重定向:类型 R_X86_64_PLT32、Addend -4、符号 print 。


在替换代码被加载到目标进程中时,我们可以确定替换代码加载的地址。假设替换代码中 func 地址为 0x7fa357ed79b0,原程序中 print 地址为 0x7fa358a9979c。那么根据之前记录的重定向信息进行计算(V = S + A - P),将 func 偏移 0xa 的位置(4 字节)写入 0xbc1dde,计算方法如下:0x7fa358a9979c + (-4) - 0x7fa357ed79ba = 0xbc1dde。


这样重定向之后,替换代码中的 func 函数就能正确引用到原程序中的 print 函数。最后,我们通过 GDB 观察 T 程序打入热补丁之后的行为:



以上可以看出原程序的 func 函数会跳转到 0x7fa357ed79b0 执行。



以上可以看出 0x7fa357ed79b0 是热补丁中 func 函数的入口,也能看出 0x00007fa357ed79b9 地址的指令中引用了正确的 print 符号。


我们通过 objdump、gdb 验证了替换代码的静态和动态形式,展示了自动生成热补丁的具体细节,希望借此可以让读者有更清晰的理解。

5 其他注意事项

这种前后目标文件比较生成替换代码的方法,要求我们必须拥有正在运行的目标程序的源代码。同时,在编译目标文件时强烈建议使用和目标程序相同版本的 gcc 和编译选项,使用不同的 gcc 和编译选项可能会导致目标文件和原始程序的二进制不匹配,导致不能生成正确的替换代码。不正确的替换代码可能会导致符号解析错误,进而是程序崩溃,这一点需要特别注意。

6 写在最后

本文介绍了二进制比较方式的自动生成热补丁技术,相比于上一篇文章中介绍的简单热补丁技术,优势在于:


  • 通过工具自动生成热补丁,无需手动编写热补丁,减少了人为出错的可能;

  • 可以修复本地函数和全局函数,并且不需要引入函数的依赖链;

  • 兼容多种编译型语言(C 语言、C++、汇编等),具体实现不同但思路一致。


使用这种热补丁生成技术,可以解决应用程序几乎所有的安全漏洞。例如之前出现的 QEMU CVE-2017-2615(cirrus 驱动内存越界访问),我们对有 bug 的函数都打上了热补丁,通过替换 bug 函数,实现了在线热修复。


生成热补丁是应用程序热补丁技术框架中非常关键的一个组件,本文介绍了一种自动生成热补丁的技术。完整、成熟的热补丁框架还包含了其他技术,例如多线程、管理多个热补丁、多版本管理、热补丁与程序之间的一致性检查等。接下来,在《应用程序热补丁(三)》中,我们将会对这些问题进行解答,并介绍 UCloud 应用程序热补丁技术的完整框架,对框架中各个组件进行解析。


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


原文链接:


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


2019-11-12 16:302607

评论

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

36kr企服点评启发:“信任的基础是真实,只要做到真实,一定能够建立起双向信任”

B Impact

金融信创正当时!看这家银行如何实现数据仓库与营销平台国产替换?

索信达控股

数据库 银行 数据库迁移 智能营销

【MindStudio训练营第一季】MindStudio 高精度对比随笔

Angel Wings

MindStudio

React 之 Refs 的使用和 forwardRef 的源码解读

冴羽

JavaScript react.js 前端 前端框架 React

【MindStudio训练营第一季】MindStudio 专家系统随笔

Angel Wings

mindspore MindStudio

源码解析:Dubbo3 的 Spring 适配原理与初始化流程

Apache Dubbo

Java 开源 微服务 dubbo

4.0体验站|OceanBase 4.0,从分布式到单机,从单机到分布式

OceanBase 数据库

数据库 oceanbase

AWS CEO Adam Selipsky 演讲 Keynote @ re:Levent2022

B Impact

世界杯太精彩了,带大家用Python做个足球游戏,边玩游戏边看比赛

Lansonli

Python游戏 Python足球游戏 世界杯足球游戏

实时数据赋能制造业产能升级:详解半导体和汽车制造行业最佳实践(活动报名)

tapdata

制造业 数据集成 汽车制造 实时数据 半导体行业

亚马逊CTO Werner Vogels 演讲Keynote:世界一直无序地运转向前 @AWS re:Invent 2022

B Impact

【MindStudio训练营第一季】MindStudio Profiling随笔

Angel Wings

华为 AI 调优 MindStudio Ascend

喜讯!YMatrix 当选新能源汽车国家大数据联盟理事单位

YMatrix 超融合数据库

数据库 新能源汽车 新能源 超融合数据库 YMatrix

一站式动态多环境建设案例

阿里巴巴中间件

阿里云 微服务 云原生 中间件 客户案例

通过认证|龙智正式成为Atlassian云专业伙伴

龙智—DevSecOps解决方案

云原生

代码安全与质量 | 在这个充满变数的时代,花小钱办大事

龙智—DevSecOps解决方案

代码质量 代码安全检测 代码安全 安全防护

【MindStudio训练营第一季】MindStudio 可视化AI应用开发体验随笔

Angel Wings

MindStudio

HummerRisk 入门3:开发手册

HummerCloud

云安全 云原生安全 12月月更

如何设计业务异地多活架构 - week7

in9

听软件测试自动化“领导者”讲解如何降本、增效与提质

龙智—DevSecOps解决方案

测试 自动化测试 测试自动化

版本控制 | 一文了解什么是组件化开发,以及如何从单体架构转向组件化开发

龙智—DevSecOps解决方案

组件化 组件化开发

软件测试 | 测试核心:如何减少线上故障?

测试人

软件测试 软件质量 自动化测试 测试开发

携程商旅CEO张勇:TMC不止一站式解决方案 携程商旅推出“产品云图”

携程商旅

可观测性神器之Micrometer

宋小生

监控 可观测性 micrometer

2022年第三季度汽车品牌智能网联竞争力指数(ICVCI)分析

易观分析

汽车 易观分析 智能网联

中国敏捷十年实践者分享:敏捷教练的自我修为

华为云开发者联盟

云计算 华为云 12 月 PK 榜

实践案例丨CenterNet-Hourglass论文复现

华为云开发者联盟

人工智能 华为云 12 月 PK 榜

隐藏复杂、抽象概念,「技术无感化」 ——The Future of Database2022 | 黄东旭新番

B Impact

Meta Force佛萨奇2.0元宇宙项目系统开发技术讲解方案

I8O28578624

Golang中利用BPF进行动态追踪

MatrixOrigin

Go 数据库 云原生 MatrixOrigin MatrixOne

神秘新品即将来袭!大上科技开启护眼新未来倒计时

硬科技星球

应用程序热补丁(二): 自动生成热补丁_文化 & 方法_王超_InfoQ精选文章