【QCon】精华内容上线92%,全面覆盖“人工智能+”的典型案例!>>> 了解详情
写点什么

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

  • 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:302120

评论

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

netty案例,netty4.1中级拓展篇七《Netty请求响应同步通信》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1基础入门篇三《NettyServer字符串解码器》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1基础入门篇四《NettyServer收发数据》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1基础入门篇五《NettyServer字符串编码器》

小傅哥

Java Netty

netty案例,netty4.1中级拓展篇一《Netty与SpringBoot整合》

小傅哥

Java Netty

netty案例,netty4.1中级拓展篇四《Netty传输文件、分片发送、断点续传》

小傅哥

Netty 小傅哥

netty案例,netty4.1中级拓展篇八《Netty心跳服务与断线重连》

小傅哥

Netty 小傅哥

netty案例,netty4.1基础入门篇零《初入JavaIO之门BIO、NIO、AIO实战练习》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1基础入门篇十二《简单实现一个基于Netty搭建的Http服务》

小傅哥

Java Netty

netty案例,netty4.1中级拓展篇九《Netty集群部署实现跨服务端通信的落地方案》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1基础入门篇二《NettyServer接收数据》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1基础入门篇十《关于ChannelOutboundHandlerAdapter简单使用》

小傅哥

Netty 小傅哥

程序开发中的持续集成、持续交付、持续部署

石云升

持续集成 持续交付 持续部署 自动化部署

netty案例,netty4.1中级拓展篇十一《Netty基于ChunkedStream数据流切块传输》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1中级拓展篇十二《Netty流量整形数据流速率控制分析与实战》

小傅哥

Netty 小傅哥

netty案例,netty4.1基础入门篇六《NettyServer群发消息》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1基础入门篇七《嗨!NettyClient》

小傅哥

Netty 小傅哥

netty案例,netty4.1中级拓展篇三《Netty传输Java对象》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1中级拓展篇十《Netty接收发送多种协议消息类型的通信处理方案》

小傅哥

Java Netty 小傅哥

大龄程序员的自我介绍 v 0.1

escray

学习 面试 自我介绍

netty案例,netty4.1中级拓展篇二《Netty使用Protobuf传输数据》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1中级拓展篇五《基于Netty搭建WebSocket,模仿微信聊天页面》

小傅哥

Java Netty 小傅哥

netty案例,netty4.1高级应用篇二,手写RPC框架第二章《netty通信》

小傅哥

Netty 小傅哥

netty案例,netty4.1基础入门篇一《嗨!NettyServer》

小傅哥

Java Netty

netty案例,netty4.1基础入门篇九《自定义编码解码器,处理半包、粘包数据》

小傅哥

Java Netty

netty案例,netty4.1基础入门篇十一《netty udp通信方式案例Demo》

小傅哥

Java Netty

netty案例,netty4.1中级拓展篇六《SpringBoot+Netty+Elasticsearch收集日志信息数据存储》

小傅哥

Java Netty

世界很大,我想去看看

escray

学习 面试

netty案例,netty4.1基础入门篇八《NettyClient半包粘包处理、编码解码处理、收发数据方式》

小傅哥

Netty 小傅哥

netty案例,netty4.1中级拓展篇十三《Netty基于SSL实现信息传输过程中双向加密验证》

小傅哥

Netty 小傅哥

netty案例,netty4.1高级应用篇一,手写RPC框架第一章《自定义配置xml》

小傅哥

Java Netty

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