[转载] 学会黑科技,一招搞定 iOS 14.2 的 libffi crash

[转载] 学会黑科技,一招搞定 iOS 14.2 的 libffi crash

原文地址

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

descript

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

libffi 是什么

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

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

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

ffi 的使用

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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 开始说起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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 (1sizeof (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 pageremap 到我们事先在代码中汇编写的 ffi_closure_remap_trampoline_table_page

其结构如图所示:

descript

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
ffi_status

ffi_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

1
2
3
4
5
6
7
adr x16, -PAGE_MAX_SIZE

ldp x17, x16, [x16]

br x16

nop

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 动态remap的 page
.align PAGE_MAX_SHIFT

CNAME(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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# 静态分配的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

.text

CNAME(_ffi_static_trampoline_page1):

_ffi_local_forwarding_bridge:

adrp x17, ffi_closure_static_trampoline_table_page_start@PAGE;// text
page

sub x16, x16, x17;// offset

adrp x17, _ffi_static_trampoline_data_page1@PAGE;// data page

add x16, x16, x17;// data address

ldp x17, x16, [x16];// x17 closure x16 start

br x16

nop

nop

.align PAGE_MAX_SHIFT

CNAME(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, #0

b _ffi_local_forwarding_bridge

nop

nop

adr x16, #0

b _ffi_local_forwarding_bridge

nop

nop

adr x16, #0

b _ffi_local_forwarding_bridge

nop

nop

adr x16, #0

b _ffi_local_forwarding_bridge

nop

nop

adr x16, #0

b _ffi_local_forwarding_bridge

nop

nop

// 5 * 4

.rept (PAGE_MAX_SIZE*5-5*4) / FFI_TRAMPOLINE_SIZE

adr x16, #0

b _ffi_local_forwarding_bridge

nop

nop

.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
-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道