长亭百川云 - 文章详情

IOS PAC实现详解

kernsec

108

2024-07-13

1. 内核pac key初始化

common_start

mrs     x0, S3_4_C15_C0_4

and     x1, x0, #0x2

cbz     x1, 0xfffffff0081384d0

orr     x0, x0, #0x1

orr     x0, x0, #0x4

msr     S3_4_C15_C0_4, x0

Isb

首先探测下S3_4_C15_C0_4寄存器的第2个bit是否为0, 如果为0, 则一直等待下去,否则把其第1个和第3个比特位置1,结合后面对_ml_set_kernelkey_enabled的分析,可以推测出S3_4_C15_C0_4寄存器是苹果公司为增强pac而加入的控制寄存器。

LDR             X0, =0xFEEDFACEFEEDFACF

MSR             #0, c2, c1, #2, X0 ; APIBKeyLo_EL1

ADD             X0, X0, #1

MSR             #0, c2, c1, #3, X0 ; APIBKeyHi_EL1

ADD             X0, X0, #1

MSR             #0, c2, c2, #2, X0 ; APDBKeyLo_EL1

ADD             X0, X0, #1

MSR             #0, c2, c2, #3, X0 ; APDBKeyHi_EL1

ADD             X0, X0, #1

MSR             #4, c15, c1, #0, X0; ???

ADD             X0, X0, #1

MSR             #4, c15, c1, #1, X0; ???

ADD             X0, X0, #1

MSR             #0, c2, c1, #0, X0 ; APIAKeyLo_EL1

ADD             X0, X0, #1

MSR             #0, c2, c1, #1, X0 ; APIAKeyHi_EL1

ADD             X0, X0, #1

MSR             #0, c2, c2, #0, X0 ; APDAKeyLo_EL1

ADD             X0, X0, #1

MSR             #0, c2, c2, #1, X0 ; APDAKeyHi_EL1

ADD             X0, X0, #1

MSR             #0, c2, c3, #0, X0 ; APGAKeyLo_EL1

ADD             X0, X0, #1

MSR             #0, c2, c3, #1, X0 ; APGAKeyHi_EL1

Iphone12使用固定值0xFEEDFACEFEEDFACF依次初始化APIAKey_EL1、APIBKey_EL1、APDAKey_EL1、APDBKey_EL1、APGAKey_EL1寄存器。高版本的Iphone内核是否也使用固定值有待确认。

MOVK            X0, #0,LSL#48

MOVK            X0, #0,LSL#32

MOVK            X0, #0x3454,LSL#16

MOVK            X0, #0x593D ; 0x3454593d

ORR             X0, X0, #0x40000000 ; 0x7454593d

MSR             #0, c1, c0, #0, X0 ; SCTLR_EL1

很奇怪这里sctlr_el1只设置了EnIB位(笔者翻遍了代码,也没发现对sctlr_el1其他pac比特位的使用)。

2. 用户进程pac key初始化

通过execve执行一个binary时,XNU内核会进行新进程pac key的初始化,XNU将每个进程的pac key保存在一个叫做shared region的进程内存区域,这个区域可以被其他进程共享代码和数据,用来加快进程的启动速度,通常情况下一个进程只有一个shared region区域,但是在支持pac的情况下,shared region根据不同的shared_region_id会有不同的shared region区域,通过一个队列_shared_region_jop_key_queue链接起来,每个节点是一个struct shared_region_jop_key_map类型的结构体,xnu source code有如下定义:

typedef struct shared_region_jop_key_map {

        queue_chain_t  srk_queue;

        char           *srk_shared_region_id;

        uint64_t       srk_jop_key;

        os_refcnt_t    srk_ref_count;         /* count of tasks active with this shared_region_id */

} *shared_region_jop_key_map_t;

成员srk_jop_key保存的是这个region所使用的pac key。下面我们来看下pac key是如何被初始化的。

/*

void

shared_region_key_alloc(char *shared_region_id, bool inherit, uint64_t inherited_key)

 */

exec_mach_imgact->_shared_region_key_alloc

LDR             X8, [X24] ; _shared_region_jop_key_queue

CMP             X8, X24

B. EQ            loc_FFFFFFF007B33338 ;

遍历_shared_region_jop_key_queue,如果队列为空,跳转到后面去申请一个新的shared_region节点。

MOV             X9, X8

LDR             X10, [X9,#0x10]

MOV             X11, X22

LDRB            W12, [X10]

LDRB            W13, [X11]

CMP             W12, W13

B.NE            loc_FFFFFFF007B3332C

ADD             X11, X11, #1

ADD             X10, X10, #1

CBNZ            W12, loc_FFFFFFF007B3330C

B               loc_FFFFFFF007B33450 ; srk_ref_count

LDR             X9, [X9]

CMP             X9, X24

C. NE            loc_FFFFFFF007B33304

循环遍历每个shared_region节点,如果节点名与第一个参数相同,证明存在一个要匹配的节点。

__TEXT_EXEC:__text:FFFFFFF007B33450

E ADD             X0, X9, #0x20 ; ' ' ; srk_ref_count

MOV             W8, #1__TEXT_EXEC:__text:FFFFFFF007B33458                 LDADD           W8, W8, [X0] ; srk_ref_count++

将引用计数srk_ref_count加1.

CBZ             W8, loc_FFFFFFF007B33554

MOV             W10, #0xFFFFFFF

CMP             W8, W10

B. CS            loc_FFFFFFF007B33558

判断引用计数是否溢出

MOV             X22, X21 ; x21 == 0

MOV             X21, X9

CBZ             W20, loc_FFFFFFF007B33488 ; inherit == 0

LDR             X8, [X21,#0x18] ; inherit == 1 -> srk_jop_key

LDR             X9, [SP,#0x70+var_60]

CMP             X8, X9  ; arg3: inherited_key

C. NE            loc_FFFFFFF007B3356C

如果第二个参数inherit为1,并且此shared_region保存的pac key与第三个参数不相同,则panic,否则直接返回,不需要更新pac key。

下面看下队列如果为空,或者没有找到要匹配到的shared_region_id的后续处理流程。

ADRL            X0, _KHEAP_DEFAULT

MOV             W1, #0x28 ; '('

MOV             W2, #0

ADRL            X3, _shared_region_key_alloc.site

BL              _kalloc_ext

分配一个新的struct shared_region_jop_key_map结构体

MOV             X21, X0 ; new struct shared_region_jop_key_map

MOV             X0, X22 ; __s

BL              _strlen ; shared_region_id

计算参数1shared_region_id的长度

ADD             W28, W0, #1

ADRL            X0, _KHEAP_DATA_BUFFERS

MOV             X1, X28

MOV             W2, #0

ADRL            X3, _shared_region_key_alloc.site.3

BL              _kalloc_ext ; alloc shared_region_id buffer

分配shared_region_id内存

STR             X0, [X21,#0x10] ; set srk_shared_region_id

保存shared_region_id到struct shared_region_jop_key_map对应成员。

MOV             X1, X22 ; __source

MOV             X2, X28 ; __size

LDR             X23, [SP,#0x70+var_60]

BL              _strlcpy ; strlcpy(srk_shared_region_id, shared_region_id, size);

拷贝shared_region_id。

MOV             W8, #1

STR             W8, [X21,#0x20] ; srk_ref_count = 1

引用计数srk_ref_count初始化为1。

ADRP            X8, #_diversify_user_jop@PAGE

LDR             W9, [X8,#_diversify_user_jop@PAGEOFF]

CMP             W9, #0

CSET            W8, NE

TST             W8, W20 ; arg2: inherit

MOV             X8, #0xFEEDFACEFEEDFAD5

CSEL            X8, X23, X8, NE ; x23: arg3 inherit

如果diversify_user_jop为1并且第2个参数inherit也为1,x8被设置为第三个参数inherit,否则设置为0xFEEDFACEFEEDFAD5。

ADRL            X24, _shared_region_jop_key_queue

CBZ             W9, loc_FFFFFFF007B33444

TBNZ            W20, #0, loc_FFFFFFF007B33444

MOV             X0, X22 ; __s

BL              _strlen

CBZ             X0, loc_FFFFFFF007B33434 ; strlen(shared_region_id) == 0

如果shared_region_id长度为0, x8设置为0xFEEDFACEFEEDFAD5。我们看到用户进程的pac key居然和内核的pac key0xFEEDFACEFEEDFACF值很接近!

__TEXT_EXEC:__text:FFFFFFF007B333FC

LDR             X8, [X19]

LDR             X0, [X26,#_prng_ctx@PAGEOFF]

BLRAA           X8, X28

LDR             X8, [X19,#(qword_FFFFFFF0076E7760 - 0xFFFFFFF0076E7758)]

LDR             X0, [X26,#_prng_ctx@PAGEOFF]

MRS             X9, #0, c13, c0, #4 ; TPIDR_EL1

LDR             X9, [X9,#0x4F8]

LDRH            W1, [X9]

ADD             X3, SP, #0x70+var_58

MOV             W2, #8

BLRAA           X8, X25

LDR             X8, [SP,#0x70+var_58]

CBZ             X8, loc_FFFFFFF007B333FC

获取一个随机数,赋值给x8。

STR             X8, [X21,#0x18]

X8为要保存的pac key值。

总结以下,一个进程可以继承父进程的pac key,可以是个随机值,还可以是固定值0xFEEDFACEFEEDFAD5。

这里还存在另一个安全问题,虽然把生成随机数的两个函数指针都使用pac签名过,但是pac计算过程中使用的context值居然是个固定的4个字节值:0x2ABE和0x9BF6。这会弱化随机数生成函数的安全性。

3. 进程之间切换pac

由于每个进程的pac key可能不同,所以在进程切换的时候,也需要切换pac key。

_Switch_context

STR             XZR, [X3,#0x78] ; SS64_KERNEL_PC

MOV             W4, #0x100004 ; PSR64_KERNEL_POISON

STR             W4, [X3,#0x80] ; SS64_KERNEL_CPSR

STP             X0, X1, [SP,#var_10]!

STP             X2, X3, [SP,#0x10+var_20]!

STP             X4, X5, [SP,#0x20+var_30]!

MOV             X0, X3

MOV             X1, #0

MOV             W2, W4

MOV             X3, X30

MOV             X4, X16

MOV             X5, X17

BL              _ml_sign_kernel_thread_state ; compute old thread pac code.

在进程切换前,首先计算处当前进程的thread state pac值。linux内核也提供了pac服务,但是没有对thread状态做校验。

__TEXT_EXEC:__text:FFFFFFF008139A8C _ml_sign_kernel_thread_state            ; CODE PACGA           X1, X1, X0

AND             X2, X2, #0xFFFFFFFFDFFFFFFF

PACGA           X1, X2, X1

PACGA           X1, X3, X1

PACGA           X1, X4, X1

PACGA           X1, X5, X1

STR             X1, [X0,#0x88] ; struct arm_kernel_saved_state->jophash

RET

Pacga指令用于计算大块的内存区域,可以看到XNU使用pacga指令依次对x0: The ARM context pointer、x1: PC value to sign、x2: CPSR value to sign、x3: LR to sign、x16、x17做计算生成pac。

X0是一个struct arm_kernel_save_state的数据结构:

 struct arm_kernel_saved_state {

uint64_t x[12];     /* General purpose registers x16-x28 */

uint64_t fp;        /* Frame pointer x29 */

uint64_t lr;        /* Link register x30 */

uint64_t sp;        /* Stack pointer x31 */

uint64_t pc;        /* Program counter */

uint32_t cpsr;      /* Current program status register */

uint64_t jophash;

} __attribute__((aligned(16)));

在生成当前进程的thread state pac后,开始校验下要切换的进程thread state状态,如果校验不过,证明要切换的进程状态被篡改过,系统直接panic。

LDR             X3, [X2,#0x498] ; new TH_KSTACKPTR

MOV             X20, X0

MOV             X21, X1

MOV             X22, X2

MOV             X0, X3

LDR             W2, [X0,#0x80] ; new SS64_KERNEL_CPSR

DMB             LD

LDR             X1, [X0,#0x78] ; new SS64_KERNEL_PC

LDP             X16, X17, [X0]

MOV             X25, X3

MOV             X26, X4

MOV             X27, X5

MOV             X23, X1

MOV             X24, X2

LDR             X3, [X0,#0x68]

MOV             X4, X16

MOV             X5, X17

BL              _ml_check_kernel_signed_state ; check new thread pac code.

__TEXT_EXEC:__text:FFFFFFF008139AEC _ml_check_kernel_signed_state           ; CODE PACGA           X1, X1, X0

AND             X2, X2, #0xFFFFFFFFDFFFFFFF

PACGA           X1, X2, X1

PACGA           X1, X3, X1

PACGA           X1, X4, X1

PACGA           X1, X5, X1

LDR             X2, [X0,#0x88]

从struct arm_kernel_saved_state结构体提取pac code。

CMP             X1, X2

B.NE            loc_FFFFFFF008139B14

RET

相同返回上层函数。

__TEXT_EXEC:__text:FFFFFFF008139B14 ;

PACIBSP

STP             X29, X30, [SP,#var_10]!

MOV             X29, SP

BL              _panic

不相同则panic系统。

LDR             X5, [X2,#0x4F8]

MOV             W6, #0

LDR             X3, [X2,#0x508]

LDR             X4, [X5,#0x200]

CMP             X3, X4

B.EQ            loc_FFFFFFF008138B48

STR             X3, [X5,#0x200]

MSR             #0, c2, c1, #2, X3 ; APIBKeyLo_EL1

ADD             X3, X3, #1

MSR             #0, c2, c1, #3, X3 ; APIBKeyHi_EL1

ADD             X3, X3, #1

MSR             #0, c2, c2, #2, X3 ; APDBKeyLo_EL1

ADD             X3, X3, #1

MSR             #0, c2, c2, #3, X3 ; APDBKeyHi_EL1

加载新进程的pac key到对应的系统寄存器。

4 内核pac与用户进程pac切换

4.1 进入内核

进程每次通过系统调用进入内核或者发生错误进入内核异常处理流程时,需要把进程的pac key切换为内核的pac key。

__TEXT_EXEC:__text:FFFFFFF008131410 fleh_dispatch64

MOVK            X2, #0xFEED,LSL#48

MOVK            X2, #0xFACE,LSL#32

MOVK            X2, #0xFEED,LSL#16

MOVK            X2, #0xFAD5 ; 0xFEEDFACEFEEDFAD5

MRS             X3, #0, c13, c0, #4 ; TPIDR_EL1

LDR             X3, [X3,#0x4F8]

LDR             X4, [X3,#0x208]

CMP             X2, X4

B.EQ            loc_FFFFFFF0081314F0

MSR             #0, c2, c1, #0, X2 ; APIAKeyLo_EL1

ADD             X4, X2, #1

MSR             #0, c2, c1, #1, X4 ; APIAKeyHi_EL1

ADD             X4, X4, #1

MSR             #0, c2, c2, #0, X4 ; APDAKeyLo_EL1

ADD             X4, X4, #1

MSR             #0, c2, c2, #1, X4 ; APDAKeyHi_EL1

STR             X2, [X3,#0x208]

在进入内核时,首先要更新APIAKey_EL1和APDAKey_EL1为固定值0xFEEDFACEFEEDFAD5极其增值。

MOV             X21, X1

MOV             X20, X30

MOV             X1, X22

MOV             W2, W23

MOV             X3, X20

MOV             X4, X16

MOV             X5, X17

BL              _ml_sign_thread_state

更新完pac key值,马上对当前进程的thread state进行签名。

4.2 退出内核

__TEXT_EXEC:__text:FFFFFFF00813197C exception_return_unint_tpidr_x3_dont_trash_x18

MOV             X0, SP

LDR             W2, [X0,#arg_110]

LDR             X1, [X0,#arg_108]

LDP             X16, X17, [X0,#arg_88]

MOV             X22, X3

MOV             X23, X4

MOV             X24, X5

MOV             X20, X1

MOV             X21, X2

LDR             X3, [X0,#arg_F8]

MOV             X4, X16

MOV             X5, X17

BL              _ml_check_signed_state

在退出内核到用户态前,需要再次校验下当前进程的thread state,如果发生错误,则panic系统。

LDR             X1, [X2,#0x510]

LDR             X2, [X2,#0x4F8]

LDR             X3, [X2,#0x208]

CMP             X1, X3

B.EQ            loc_FFFFFFF008131A3C

MSR             #0, c2, c1, #0, X1 ; APIAKeyLo_EL1

ADD             X3, X1, #1

MSR             #0, c2, c1, #1, X3 ; APIAKeyHi_EL1

ADD             X3, X3, #1

MSR             #0, c2, c2, #0, X3 ; APDAKeyLo_EL1

ADD             X3, X3, #1

MSR             #0, c2, c2, #1, X3 ; APDAKeyHi_EL1

STR             X1, [X2,#0x208]

恢复进程使用的pac key。

5 进程thread state pac的初始化

前面提到在进程切换时,需要验证进程的thread state pac,那么它是在何时初始化的呢?

答案在进程初始化stack时进行thread state的pac计算。

_machine_stack_attach

STR             X1, [X0,#0x78] ; struct arm_kernel_saved_state->pc

MOV             X2, #0x100004

STR             W2, [X0,#0x80] ; struct arm_kernel_saved_state->cpsr

ADRL            X3, _thread_continue

STR             X3, [X0,#0x68] ; struct arm_kernel_saved_state->lr

MOV             X4, XZR

MOV             X5, XZR

STP             X4, X5, [X0]

MOV             X6, X30

BL              _ml_sign_kernel_thread_state ; sign thread state.

6. Ppl相关

Ppl提供了两个服务用来给用户进程数据进行签名与验签。

6.1 _pmap_sign_user_ptr

__TEXT_EXEC:__text:FFFFFFF007B609A0 _pmap_sign_user_ptr

ADD             X29, SP, #0x30

MOV             X19, X0

MRS             X23, #3, c4, c2, #1 ; DAIFSet

TBNZ            W23, #7, loc_FFFFFFF007B609D0 ; TPIDR_EL1

MSR             #6, #7

关闭DAIF中断

MRS             X8, #0, c13, c0, #4 ; TPIDR_EL1

LDR             X8, [X8,#0x4F8]

LDR             X20, [X8,#0x208]

MOV             W0, #0

MOV             X1, X3

BL              _ml_set_kernelkey_enabled

将0作为参数传递给_ml_set_kernelkey_enabled。

__TEXT_EXEC:__text:FFFFFFF008139374 _ml_set_kernelkey_enabled               

MRS             X2, #0, c13, c0, #4 ; TPIDR_EL1

LDR             X2, [X2,#0x4F8]

LDR             X3, [X2,#0x208]

CMP             X1, X3

B. EQ            loc_FFFFFFF0081393A8 ; S3_4_C15_C0_4

如果当前进程的pac key和用户传递进来的pac key相等,则直接跳转到后面。

MSR             #0, c2, c1, #0, X1 ; APIAKeyLo_EL1

ADD             X3, X1, #1

MSR             #0, c2, c1, #1, X3 ; APIAKeyHi_EL1

ADD             X3, X3, #1

MSR             #0, c2, c2, #0, X3 ; APDAKeyLo_EL1

ADD             X3, X3, #1

MSR             #0, c2, c2, #1, X3 ; APDAKeyHi_EL1

STR             X1, [X2,#0x208]

依次切换系统寄存器APIAKey_EL1、APDAKey_EL1,可以看到ppl只用key A来签名用户代码或数据。

MRS             X1, #4, c15, c0, #4 ; S3_4_C15_C0_4

ORR             X3, X1, #4

AND             X2, X1, #0xFFFFFFFFFFFFFFFB

CMP             W0, #0

CSEL            X1, X2, X3, EQ

MSR             #4, c15, c0, #4, X1 ; S3_4_C15_C0_4

ISB

RET

如果传递给_ml_set_kernelkey_enabled函数的第一个参数为0,那么将S3_4_C15_C0_4的第3个bit置0,否则置1。

回到_pmap_sign_user_ptr

CMP             W22, #2

B.EQ            loc_FFFFFFF007B609FC

CBNZ            W22, loc_FFFFFFF007B60A34

PACIA           X19, X21

B               loc_FFFFFFF007B60A00

PACDA           X19, X21

如果第2个参数为2,则使用pacda对第1个参数签名,如果为0,使用pacia进行签名,如果不是0,也不是2, 则panic系统。

MOV             W0, #1

MOV             X1, X20

BL              _ml_set_kernelkey_enabled

将1作为参数传递给_ml_set_kernelkey_enabled。

_ml_set_kernelkey_enabled(0, pac_key)

...

Pacia/pacib

...

_ml_set_kernelkey_enabled(1, pac_key)

因此我们可以推测出S3_4_C15_C0_4的第2个bit为上锁功能, 0代表上锁,1代表解锁。

6.2 _pmap_auth_user_ptr

AUTIA           X19, X21

B               loc_FFFFFFF007B60AD4

AUTDA           X19, X21

B               loc_FFFFFFF007B60AD4

AUTDB           X19, X21

B               loc_FFFFFFF007B60AD4

AUTIB           X19, X21

这里我们看到_pmap_auth_user_ptr在验签的时候还使用了key b,而_ml_set_kernelkey_enabled只设置了key a, 说明了key a是进程相关的,而key b是进程不相关的, XNU源码里定义了以下几种类型的pac key:

EXTERNAL_HEADERS/ptrauth.h

typedef enum {

  ptrauth_key_asia = 0,

  ptrauth_key_asib = 1,

  ptrauth_key_asda = 2,

  ptrauth_key_asdb = 3,

  /* A process-independent key which can be used to sign code pointers.

     Signing and authenticating with this key is a no-op in processes

     which disable ABI pointer authentication. */

  ptrauth_key_process_independent_code = ptrauth_key_asia,

  /* A process-specific key which can be used to sign code pointers.

     Signing and authenticating with this key is enforced even in processes

     which disable ABI pointer authentication. */

  ptrauth_key_process_dependent_code = ptrauth_key_asib,

  /* A process-independent key which can be used to sign data pointers.

     Signing and authenticating with this key is a no-op in processes

     which disable ABI pointer authentication. */

  ptrauth_key_process_independent_data = ptrauth_key_asda,

  /* A process-specific key which can be used to sign data pointers.

     Signing and authenticating with this key is a no-op in processes

     which disable ABI pointer authentication. */

  ptrauth_key_process_dependent_data = ptrauth_key_asdb,

  /* The key used to sign C function pointers.

     The extra data is always 0. */

  ptrauth_key_function_pointer = ptrauth_key_process_independent_code,

  /* The key used to sign return addresses on the stack.

     The extra data is based on the storage address of the return address.

     On ARM64, that is always the storage address of the return address plus 8

     (or, in other words, the value of the stack pointer on function entry) */

  ptrauth_key_return_address = ptrauth_key_process_dependent_code,

  /* The key used to sign frame pointers on the stack.

     The extra data is based on the storage address of the frame pointer.

     On ARM64, that is always the storage address of the frame pointer plus 16

     (or, in other words, the value of the stack pointer on function entry) */

  ptrauth_key_frame_pointer = ptrauth_key_process_dependent_data,

  /* The key used to sign block function pointers, including:

       invocation functions,

       block object copy functions,

       block object destroy functions,

       __block variable copy functions, and

       __block variable destroy functions.

     The extra data is always the address at which the function pointer

     is stored.

     Note that block object pointers themselves (i.e. the direct

     representations of values of block-pointer type) are not signed. */

  ptrauth_key_block_function = ptrauth_key_asia,

  /* The key used to sign C++ v-table pointers.

     The extra data is always 0. */

  ptrauth_key_cxx_vtable_pointer = ptrauth_key_asda,

  /* Other pointers signed under the ABI use private ABI rules. */

} ptrauth_key;

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2