写点什么

学会黑科技,一招搞定 iOS 14.2 的 libffi crash

  • 2021-05-17
  • 本文字数:3868 字

    阅读完需:约 13 分钟

学会黑科技,一招搞定 iOS 14.2 的 libffi crash

苹果升级 14.2,全球 iOS 遭了秧。libffi 在 iOS14.2 上发生了 crash, 我司的许多 App 深受困扰,有许多基础库都是用了 libffi。



经过定位,发现是 vmremap 导致的 code sign error。我们通过使用静态 trampoline 的方式让 libffi 不需要使用 vmremap,解决了这个问题。这里就介绍一下相关的实现原理。

libffi 是什么

高层语言的编译器生成遵循某些约定的代码。这些公约部分是单独汇编工作所必需的。“调用约定”本质上是编译器对函数入口处将在哪里找到函数参数的假设的一组假设。“调用约定”还指定函数的返回值在哪里找到。

一些程序在编译时可能不知道要传递给函数的参数。例如,在运行时,解释器可能会被告知用于调用给定函数的参数的数量和类型。Libffi 可用于此类程序,以提供从解释器程序到编译代码的桥梁。

libffi 库为各种调用约定提供了一个便携式、高级的编程接口。这允许程序员在运行时调用调用接口描述指定的任何函数。

ffi 的使用

简单的找了一个使用 ffi 的库看一下他的调用接口


ffi_type *returnType = st_ffiTypeWithType(self.signature.returnType);NSAssert(returnType, @"can't find a ffi_type of %@", self.signature.returnType);
NSUInteger argumentCount = self->_argsCount;_args = malloc(sizeof(ffi_type *) * argumentCount) ;
for (int i = 0; i < argumentCount; i++) { ffi_type* current_ffi_type = st_ffiTypeWithType(self.signature.argumentTypes[i]); NSAssert(current_ffi_type, @"can't find a ffi_type of %@", self.signature.argumentTypes[i]); _args[i] = current_ffi_type;}
// 创建 ffi 跳板用到的 closure_closure = ffi_closure_alloc(sizeof(ffi_closure), (void **)&xxx_func_ptr);
// 创建 cif,调用函数用到的参数和返回值的类型信息, 之后在调用时会结合call convention 处理参数和返回值if(ffi_prep_cif(&_cif, FFI_DEFAULT_ABI, (unsigned int)argumentCount, returnType, _args) == FFI_OK) {
// closure 写入 跳板数据页 if (ffi_prep_closure_loc(_closure, &_cif, _st_ffi_function, (__bridge void *)(self), xxx_func_ptr) != FFI_OK) { NSAssert(NO, @"genarate IMP failed"); }} else { NSAssert(NO, @"");}
复制代码


看完这段代码,大概能理解 ffi 的操作。

  1. 提供给外界一个指针(指向 trampoline entry)

  2. 创建一个 closure, 将调用相关的参数返回值信息放到 closure 里

  3. 将 closure 写入到 trampoline 对应的 trampoline data entry 处


之后我们调用 trampoline entry func ptr 时,

  1. 会找到 写入到 trampoline 对应的 trampoline data entry 处的 closure 数据

  2. 根据 closure 提供的调用参数和返回值信息,结合调用约定,操作寄存器和栈,写入参数 进行函数调用,获取返回值。


那 ffi 是怎么找到 trampoline 对应的 trampoline data entry 处的 closure 数据 呢?


我们从 ffi 分配 trampoline 开始说起:


static ffi_trampoline_table *ffi_remap_trampoline_table_alloc (void){.....  /* Allocate two pages -- a config page and a placeholder page */  config_page = 0x0;  kt = vm_allocate (mach_task_self (), &config_page, PAGE_MAX_SIZE * 2,                    VM_FLAGS_ANYWHERE);  if (kt != KERN_SUCCESS)      return NULL;
/* Allocate two pages -- a config page and a placeholder page */ //bdffc_closure_trampoline_table_page
/* Remap the trampoline table on top of the placeholder page */ trampoline_page = config_page + PAGE_MAX_SIZE; trampoline_page_template = (vm_address_t)&ffi_closure_remap_trampoline_table_page;#ifdef __arm__ /* bdffc_closure_trampoline_table_page can be thumb-biased on some ARM archs */ trampoline_page_template &= ~1UL;#endif kt = vm_remap (mach_task_self (), &trampoline_page, PAGE_MAX_SIZE, 0x0, VM_FLAGS_OVERWRITE, mach_task_self (), trampoline_page_template, FALSE, &cur_prot, &max_prot, VM_INHERIT_SHARE); if (kt != KERN_SUCCESS) { vm_deallocate (mach_task_self (), config_page, PAGE_MAX_SIZE * 2); return NULL; }

/* We have valid trampoline and config pages */ table = calloc (1, sizeof (ffi_trampoline_table)); table->free_count = FFI_REMAP_TRAMPOLINE_COUNT/2; table->config_page = config_page; table->trampoline_page = trampoline_page;
...... return table;}
复制代码


首先 ffi 在创建 trampoline 时,会分配两个连续的 page

trampoline page 会 remap 到我们事先在代码中汇编写的 ffi_closure_remap_trampoline_table_page。

其结构如图所示:

当我们 ffi_prep_closure_loc(_closure, &_cif, _st_ffi_function, (__bridge void *)(self), entry1)) 写入 closure 数据时, 会写入到 entry1 对应的 closuer1。


ffi_statusffi_prep_closure_loc (ffi_closure *closure,                      ffi_cif* cif,                      void (*fun)(ffi_cif*,void*,void**,void*),                      void *user_data,                      void *codeloc){......  if (cif->flags & AARCH64_FLAG_ARG_V)      start = ffi_closure_SYSV_V; // ffi 对 closure的处理函数  else      start = ffi_closure_SYSV;
void **config = (void**)((uint8_t *)codeloc - PAGE_MAX_SIZE); config[0] = closure; config[1] = start;......}
复制代码


这是怎么对应到的呢? closure1 和 entry1 距离其所属 Page 的 offset 是一致的,通过 offset,成功建立 trampoline entry 和 trampoline closure 的对应关系。


现在我们知道这个关系,我们通过代码看一下到底在程序运行的时候 是怎么找到 closure 的。

这四条指令是我们 trampoline entry 的代码实现,就是 ffi 返回的 xxx_func_ptr


adr x16, -PAGE_MAX_SIZEldp x17, x16, [x16]br x16nop
复制代码


通过 .rept 我们创建 PAGE_MAX_SIZE / FFI_TRAMPOLINE_SIZE 个跳板,刚好一个页的大小


# 动态remap的 page.align PAGE_MAX_SHIFTCNAME(ffi_closure_remap_trampoline_table_page):.rept PAGE_MAX_SIZE / FFI_TRAMPOLINE_SIZE  # 这是我们的 trampoline entry, 就是ffi生成的函数指针  adr x16, -PAGE_MAX_SIZE                         // 将pc地址减去PAGE_MAX_SIZE, 找到 trampoine data entry  ldp x17, x16, [x16]                             // 加载我们写入的 closure, start 到 x17, x16  br x16                                          // 跳转到 start 函数  nop        /* each entry in the trampoline config page is 2*sizeof(void*) so the trampoline itself cannot be smaller that 16 bytes */.endr
复制代码


通过 pc 地址减去 PAGE_MAX_SIZE 就找到对应的 trampoline data entry 了。

静态跳板的实现

由于代码段和数据段在不同的内存区域。


我们此时不能通过 像 vmremap 一样分配两个连续的 PAGE,在寻找 trampoline data entry 只是简单的-PAGE_MAX_SIZE 找到对应关系,需要稍微麻烦点的处理。


主要是通过 adrp 找到_ffi_static_trampoline_data_page1 和 _ffi_static_trampoline_page1的起始地址,用 pc-_ffi_static_trampoline_page1的起始地址计算 offset,找到 trampoline data entry。


# 静态分配的page#ifdef __MACH__#include <mach/machine/vm_param.h>
.align 14.data.global _ffi_static_trampoline_data_page1_ffi_static_trampoline_data_page1: .space PAGE_MAX_SIZE*5.align PAGE_MAX_SHIFT.textCNAME(_ffi_static_trampoline_page1):
_ffi_local_forwarding_bridge:adrp x17, ffi_closure_static_trampoline_table_page_start@PAGE;// text pagesub x16, x16, x17;// offsetadrp x17, _ffi_static_trampoline_data_page1@PAGE;// data pageadd x16, x16, x17;// data addressldp x17, x16, [x16];// x17 closure x16 startbr x16nopnop.align PAGE_MAX_SHIFTCNAME(ffi_closure_static_trampoline_table_page):
#这个label 用来adrp@PAGE 计算 trampoline 到 trampoline page的offset#留了5个用来调试。# 我们static trampoline 两条指令就够了,这里使用4个,和remap的保持一致ffi_closure_static_trampoline_table_page_start:adr x16, #0b _ffi_local_forwarding_bridgenopnop
adr x16, #0b _ffi_local_forwarding_bridgenopnop
adr x16, #0b _ffi_local_forwarding_bridgenopnop
adr x16, #0b _ffi_local_forwarding_bridgenopnop
adr x16, #0b _ffi_local_forwarding_bridgenopnop
// 5 * 4.rept (PAGE_MAX_SIZE*5-5*4) / FFI_TRAMPOLINE_SIZEadr x16, #0b _ffi_local_forwarding_bridgenopnop.endr
.globl CNAME(ffi_closure_static_trampoline_table_page)FFI_HIDDEN(CNAME(ffi_closure_static_trampoline_table_page))#ifdef __ELF__ .type CNAME(ffi_closure_static_trampoline_table_page), #function .size CNAME(ffi_closure_static_trampoline_table_page), . - CNAME(ffi_closure_static_trampoline_table_page)#endif#endif
复制代码


本文转载自:字节跳动技术团队(ID:BytedanceTechBlog)

原文链接:学会黑科技,一招搞定 iOS 14.2 的 libffi crash

2021-05-17 13:002056

评论

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

Spring Boot「05」Annotations 02

Samson

Java Spring Boot 学习笔记 技术笔记 10月月更

电商秒杀系统设计

张立奎

转转推荐场景EE题解决思路

转转技术团队

深度学习 推荐系统

10分钟了解sql注入--报错注入(二)

贤鱼很忙

sql 网络安全 10月月更

技术分享必备de终端录制神器

小鑫同学

前端 Node 10月月更

上手JavaScript基准测试

小鑫同学

前端 Node 10月月更

混合云中合规管理的思考

HummerCloud

云安全 混合云 安全合规检测 10月月更

算法策略的主动选择,拒绝if...else...(策略模式+简单工厂模式)

小鑫同学

前端 Node 10月月更

零信任时代企业如何提升访问权限的安全?

FinClip

容器云PaaS平台建设中应关注的重点和难点

穿过生命散发芬芳

10月月更 容器云PaaS

15分钟了解sql注入(一) union注入

贤鱼很忙

网络安全 Web SQL注入 10月月更

使用PlopJs让开发变得更高效

小鑫同学

前端 Node 10月月更

Vue组件入门(四)组件注册

Augus

Vue 3 10月月更

从一个工程师成长看自动化运维的过程

阿泽🧸

自动化运维 10月月更

开发中遇到跨域我选择这么做

小鑫同学

前端 Node 10月月更

Python进阶(十八)Python3爬虫小试牛刀之爬取CSDN博客个人信息

No Silver Bullet

Python 数据分析 10月月更

活跃开发者数量仅次于以太坊,波卡凭什么?

One Block Community

区块链 开发者 波卡生态

Flash软件应用项目(二)

张立梵

设计师 Flash 10月月更

Rust编程语言中的定义类型

小鑫同学

前端 Node 10月月更

数据导出Excel实战

卢卡多多

Excel 数据导出 10月月更

【玩转云函数】打通Github到企微的消息通知

小鑫同学

前端 Node 10月月更

桌面端开发(Tauri)开启第一篇

小鑫同学

前端 Node 10月月更

FinClip | 2022 年 9月产品更新放送

FinClip

免杀实战之面向PHP的WebShell免杀

网络安全学海

网络安全 安全 信息安全 渗透测试 WEB安全

35分钟了解sql注入-盲注(三)

贤鱼很忙

sql 网络安全 10月月更

【分享】前端线上紧急排查工具

小鑫同学

前端 Node 10月月更

开箱体验Rust,Come on!!!

小鑫同学

前端 Node 10月月更

H5加载Android本地路径图片

小鑫同学

前端 Node 10月月更

【kafka问题】记一次kafka消费者未接收到消息问题

石臻臻的杂货铺

Kafk 10月月更

Web3.0杂谈-#005(52/100)

hackstoic

元宇宙 Web3.0

Python进阶(十六)正则表达式

No Silver Bullet

Python 正则表达式 10月月更

学会黑科技,一招搞定 iOS 14.2 的 libffi crash_移动_字节跳动技术团队_InfoQ精选文章