11

IOS PAC 实现详解

 2 years ago
source link: https://paper.seebug.org/1777/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

作者: wzt
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送! 投稿邮箱:[email protected]

内核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_EL1APIBKey_EL1APDAKey_EL1APDBKey_EL1APGAKey_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比特位的使用)。

用户进程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_idstruct 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。这会弱化随机数生成函数的安全性。

进程之间切换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到对应的系统寄存器。

内核pac与用户进程pac切换

进程每次通过系统调用进入内核或者发生错误进入内核异常处理流程时,需要把进程的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_EL1APDAKey_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进行签名。

__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。

进程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.

Ppl相关

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

_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_EL1APDAKey_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代表解锁。

_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;

Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1777/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK