被冰封的 Bug:Fishhook Crash 修复纪实

作者:郝连福,业界资深计算机技术专家,现任声网Agora 首席前端架构师。先后担任过 Principal Engineer/Engineering Director(UTStarcom)、Sr. architect(Intel)、T4 architect(YY)等职,曾设计开发电信核心网专用操作系统、高性能TCP/IP协议栈、以及声网SDK架构重构等重大项目。
引言
本文是声网Agora 与 RTC 开发者社区共同发起的 Dev for Dev(Developer for Developer)互动创新实践活动的开篇,同时也是开源技术爱好者在一线工作中的真实记录。文中遇到的情况颇具代表性,特整理分享出来以飨读者。
通常在 iOS 中实现应用 Hook 的方式有以下三种:
1.Method Swizzling:利用 OC(Objective C)的 Runtime 特性,动态改变 SEL(方法编号)和 IMP(方法实现)的对应关系,达到 OC 方法调用流程改变的目的,只适用于动态的 OC 方法;
2.Fishhook:FaceBook(现更名为 Meta)提供的一个动态修改链接 Mach-O 文件的工具,利用 Mach-O 文件加载原理,通过修改懒加载和非懒加载两个表的指针实现 C 函数 HOOK 的效果;适用于静态的 C 方法;
3.Cydia Substrate:原名为 Mobile Substrate,是一个强大的框架,它的主要作用是针对 OC 方法、C 函数以及函数地址进行 HOOK 操作,适用于 OC 方法、C 函数以及函数地址,亦适用于 Android 平台。
Fishhook 是一个由 Meta 公司开源的第三方框架,它能够在模拟器和设备上动态地重新绑定运行在 iOS/macOS 上的 Mach-O 二进制文件的符号,从而实现动态修改 C 语言函数,常用于应用的调试/追踪。这个框架只包含两个核心文件:fishhook.c 以及 fishhook.h 所以非常轻量,在许多企业级应用中颇受青睐。然而这个以精练著称的开源项目中,却埋藏着一个不易察觉的问题……
随着 iOS 15 Beta 版的发布,许多开发者发现了普遍的应用程序崩溃──这通常由系统兼容性问题引发,而随着排查过程的不断深入,我们发现问题并没有那么简单。起初,开发者把问题反馈到 Fishhook 之后,有不同的团体和个人贡献了好几个修复的PR,但都未能从根本上解决这个问题。在仔细分析了 iOS 和 macOS 的操作系统内核 XNU 源码后,我们最终定位到了问题的 RootCause。
对 Fishhook Crash 问题的溯源
为了定位问题,我们通常会根据现有的报错日志尝试对问题进行复现,通过调试追踪我们发现,在 iOS 15 或者 macOS 12的环境下 Fishhook 代码在重绑定符号时会100%地发生崩溃现象,正是这个崩溃导致集成了 Fishhook 的应用变的不可用。鉴于这个问题的影响很大,一些使用了fishhook项目的应用在发现问题后紧急移除了该组件以缓解其影响。
造成 fishhook 崩溃的根本原因
Fishhook 的工作原理需要 Hook 修改符号动态绑定数据段,这些数据段的默认权限一般是只读的,所以需要加上“写”权限才能修改,而问题恰好就出在这里──我们在排查过程中发现 Fishhook 里增加“写”权限的代码存在 Bug,问题相关代码如下:

这段代码里面有3个严重错误,为了便于阅读,我们分别以红绿蓝3个颜色的框将相关代码标识出来,对这些错误的具体解释如下:
1.首先,不能仅根据 __DATA_CONST 这个 segname 来判断是否需要增加“写”权限,因为从 iOS 14.5 甚至更早的版本开始,都需要 Hook 一个叫 __AUTH_CONST 的 segment,因此只Hook 一个 __DATA_CONST 字段是不够的;
2.其次,获取当前的 vm prot 时,传错了地址,不应该是 rebindings,因为我们要写入的地址是 indirect_symbol_bindings;
3.最后,XNU 内核的 C-O-W 机制与 Linux Kernel 不同,对于 RO 的 vm segment mapping 需要显式指定 VM_PROT_COPY 才能增加“写”权限,但是 XNU BSD 的 mprotect 系统调用根本就做不到这一点,故而这句 mprotect 系统调用形同虚设,相当于什么也没做!XNU MACH 关键代码逻辑如下:

Fishhook 代码存在的上述 3 个错误叠加在一起,最终导致在修改 indirect_symbol_bindings 所指向的数据时发生了“写”保护错误,进而发生的 Crash 影响了整个应用系统。
修复 Fishhook 崩溃的最佳方法
既然我们已经找到了 Bug 位置所在,修复的思路便只需对症下药即可:
将原来写错的地址 rebindings 修改成
indirect_symbol_bindings;将 mprotect 系统调用改成使用
vm_protect系统调用,并增加VM_PROT_COPY选项;代码逻辑上修改为只有 vm_protect 系统调用执行成功时,才能去做“写”动作。
因此 Bug 修复的核心代码如下:

这里需注意,首先,为符号动态绑定的数据段增加“写”权限时一定要添加 VM_PROT_COPY 选项,否则写入操作会失败;其次,要在代码逻辑中添加“只有 vm_protect 系统调用返回成功”才能真正去执行“写”这些数据段的操作,否则就什么都不要做。
经过严格的测试和反复验证,我们彻底修复了这个 Bug,并在2021年的6月12日向 Fishhook 官方提交了 PR(https://github.com/facebook/fishhook/pull/87),Fishhook的维护团队在对比了多个修复方案后,最终选择 Merge 了我们的修复补丁并将其合并进主分支,至此该问题最终得以解决。
系统升温(级)使“冰封”的 Bug 得见天日
读者大概率会好奇,为什么在 iOS 15 或者 macOS 12 之前的版本没有这个问题呢?
事实上,在 iOS 15 或者 macOS 12 之前的操作系统自身也存在这个缺陷,对这些数据段的保护并不严谨,对应该“只读”的数据段并没有去掉“写”权限,我们调查到相关的证据如下:

在上述证据片段中,protection 数值 3 表示权限为“可读可写”,因此 Fishhook 代码里面做Hook动作的“写”操作在老版本的 iOS/macOS 中并没有任何问题。但是 iOS 15/macOS 12 新版本操作系统中对这些数据段的保护更加严格,对相应的权限做了一些调整──将本应赋予“只读”数据段的“可读可写”权限修正为“只读”,也就是说上述证据片段中 protection 的数值发生了变化,相关的证据如下:

上述代码片段中的 protection 数值1代表“只读” ──也理应如此。但正是这种“修正”与原来“不当”的配置产生了逻辑上的冲突,最终 Fishhook 的这个 Bug 在较新的 iOS 15/macOS 12 系统中暴露出来,导致了严重的崩溃问题。从代码的角度来看 Fishhook 的这个 Bug 显然是一直存在的,只是在早期的 iOS 和 macOS 版本中没有构成触发的条件,故而隐患一直被雪藏,直到相关的条件被改变。
总结
通常在应用开发过程中,本着不重复造轮子和快速上线、不断迭代的原则,我们经常会引入第三方模块,尤其是有着广泛应用的底层开源组件。但随着 IT 基础设施的变迁,系统环境会随着时间的推移不断增加新特性、抛弃旧实现,在这个过程中由于依赖问题我们的应用不可避免地会不断遭遇不可用的挑战。作为业务应用的开发者,我们必须不断提高向上游组件进行问题溯源的能力,秉持开发者的初心,取自开源、回馈开源。
Dev for Dev专栏介绍
Dev for Dev(Developer for Developer)是声网Agora 与 RTC 开发者社区共同发起的开发者互动创新实践活动。透过工程师视角的技术分享、交流碰撞、项目共建等多种形式,汇聚开发者的力量,挖掘和传递最具价值的技术内容和项目,全面释放技术的创造力。
被冰封的 Bug:Fishhook Crash 修复纪实的更多相关文章
- 关于线上的bug什么时候修复的思考
这里系统专门指的是那种用户量大的系统,比如有几百万或者上千万的注册会员.因为小系统因为用户量少,不存在这种思考,考虑有时候是多余的.另外还有内部系统,给自己公司内部人员使用的,即便是出现了问题,也不会 ...
- 用nopcomerce3.8版本的同行注意了,前2天发布3.8正式版后,作者收到一些BuG,作者修复后重新提供了一个源代码包下载.
用nopcomerce3.8版本的同行注意了,前2天发布3.8正式版后,作者收到一些BuG,作者修复后重新提供了一个源代码包下载地址,不是github上的那个链接.去作者官网论坛我那个链接地址,或关注 ...
- 在 Java 中运用动态挂载实现 Bug 的热修复
大多数 JVM 具备 Java 的 HotSwap 特性,大部分开发者认为它仅仅是一个调试工具.利用这一特性,有可能在不重启 Java 进程条件下,改变 Java 方法的实现.典型的例子是使用 IDE ...
- 写给测试人员:不是所有的bug都需要修复
用户往往对产品中各种各样的bug抱怨不已,而测试人员往往认为自己的职责就是揪出这些所有的bug并把它们全都修复.然而,这是一个误区.微软卓越测试工程总监Alan Page近日撰文,再次解释了有哪些bu ...
- 【译文】不是所有的 bug 都值得修复的
原文作者:KRISTINE PINEDO 译者:白乐航 欢迎访问网易云社区,了解更多网易技术产品运营经验. 作为软件开发者,您只需要为客户编写和交付出色的产品和功能. 但您也知道软件开发并不总是那么容 ...
- curl 中关于 CURLINFO_HEADER_SIZE 的 BUG 定位及修复
curl 官方下载页面 CentOS7 默认安装的 curl 版本太低了,需要升级为最新版. 1. 问题描述 对接了一个接口,用来下载 PDF 文件.使用 curl 下载后,文件老是报错无法打开.接口 ...
- 一例 Go 编译器代码优化 bug 定位和修复解析
https://mp.weixin.qq.com/s/Tyl6dSb7mHBuqqN6WvEuaw
- CodeSmith7代码生成器针对PostgreSQL数据库无法使用的Bug修复全过程
前言 最近公司需要将原来使用的MSSQL数据库整体迁移至pgsql,需要使用CodeSmith生成IBatisNet的配置文件.按照提示安装了Npgsql.dll后依然无法使用.引发了本次通过反编译修 ...
- [转载]基于TFS实践敏捷-修复Bug和执行代码评审
本主题阐释了这些功能,以继续这一关注虚拟敏捷团队成员的一天的教程. Peter 忙于编写一些代码以完成积压工作 (backlog) 项任务.但是,他的同事发现了一个阻碍他们工作的 Bug,他想立即修复 ...
- 常见IE浏览器bug及其修复方案(双外边距、3像素偏移、绝对定位)
1. 双外边距浮动bug IE6和更低版本中存在双外边距浮动bug,顾名思义,这个Windows bug使任何浮动元素上的外边距加倍 bug重现: <!DOCTYPE html> < ...
随机推荐
- 记录 windows RabbitMq 安装教程
安装地址:https://www.rabbitmq.com/ RabbitMq 官网下载如下两个exe文件,otp_win64_22.0.exe 文件是rabbitmq的运行环境,必须安装!!! 傻子 ...
- 实验: spring-boot 整合 fluent-mybatis 实验过程!!!!
1.参考: 简单整合,会报错误 https://segmentfault.com/a/1190000040467885?utm_source=sf-similar-article 利用maven编译, ...
- 使用 Application Loader 上传 IPA 包失败。提示信息:Please sign in with an app-specific password. You can create one at appleid.apple.com
摘自:https://www.cnblogs.com/strengthen/p/10881085.html 更新APP,使用 Application Loader 上传 IPA 包失败.提示信息:Pl ...
- 关于在Eclipse中使用EclEmma
在LAB2中,要求使用EclEmma来统计JUnit测试用例的代码覆盖度.下面就来说说如何进行基本的使用来应付实验(bushi). 在这给出完全体,可以访问下面的网址进行学习. http://www. ...
- if (()) [[]] [] 条件表达式比较示例
a.b的ASCII码是 097.098ASCII码 参考 http://www.51hei.com/mcu/4342.html 1. if (()) a=3; b=2 时,if (( a > b ...
- echarts 图表动态刷新数据
需求:每次重新加载数据,图表柱状图从零开始加载 用 myChart.clear(); 这个方式解决. 在setOption 之前用 示例: myChart.clear(); myChart.setOp ...
- python3GUI--天气预报小工具By:PyQt5(附源码)
@ 目录 一.准备工作 二.预览 1.启动 2.添加城市 三.设计流程 1.UI设计(草图) 2.UI设计(QT设计师) 3.解释 四.源代码 五.总结 之前用tk写过一款python3GUI--天气 ...
- redies概述
1.redis下载路径 Windows:https://github.com/tporadowski/redis/releases Linux:http://redis.io/download 2.安 ...
- .NetCore中配置Ef
1.在NuGet中下载 Microsoft.EntityFrameworkCore.Tools Microsoft.EntityFrameworkCore.SqlServer 2.需要在Api层.实现 ...
- 字节过滤流->缓冲流 BufferedOutputStream 用法:
1创建字节输出节点流 :FileoutputStream fos = new FileoutputStream("文件输入的路径",true);(true表示追加,false表示覆 ...