IOS 代码签名深度分析
原创 wzt 2026-01-03 08:34 浙江

OS的代码签名机制是套复杂的系统工程,本文从用户态工具到内核态处理流程对ios代码签名机制做深度分析
1 简介
IOS的代码签名机制是套复杂的系统工程,本文从用户态工具到内核态处理流程对ios代码签名机制做一个完全的分析。
2 用户态工具
苹果公司的objectc和swift语言经过编译后的二进制文件为mach-o格式,这是苹果公司自己设计的一个二进制文件格式,通过xcode的签名工具将签名信息保存在macho文件的一个特定section:LC_CODE_SIGNATURE。
苹果公司对macho的签名包含两种信息,第一个信息是文件的hash,以页为单位算出hash值,将所有的hash值保存在上述section里。另一个信息是由苹果公司签发的证书信息。证书用来证明app的来源可靠,hash值用来检测每次app启动时的完整性,确保不被篡改。
2.1 签名信息的格式
xcode调用codesign用户态工具,对app进行签名,签名信息被llvm编译器链接进macho里,它被表示成一个数据结构为:
osfmk/kern/cs_blobs.h
typedef struct __CodeDirectory {uint32_t magic; /* magic number (CSMAGIC_CODEDIRECTORY) */uint32_t length; /* total length of CodeDirectory blob */uint32_t version; /* compatibility version */uint32_t flags; /* setup and mode flags */uint32_t hashOffset; /* offset of hash slot element at index zero */uint32_t identOffset; /* offset of identifier string */uint32_t nSpecialSlots; /* number of special hash slots */uint32_t nCodeSlots; /* number of ordinary (code) hash slots */uint32_t codeLimit; /* limit to main image signature range */uint8_t hashSize; /* size of each hash in bytes */...uint8_t hashType;} CS_CodeDirectory__attribute__ ((aligned(1)));
其中hashOffset保存的就是文件以页为单位的所有sha256 hash。macos系统没有自带的工具能识别出这些hash,可以使用开源的jtool2工具,本文使用笔者自己开发的一个工具ikit工具来解析签名信息。

它可以分析ios的kernelcache,解析device tree, 提取kext,解析macho文件的hash信息、entitlements以及cms信息。
使用--sig参数提取safari的所有hash信息。
./ikit --sig -v /Applications/Safari.app/Contents/MacOS/Safari >safari.hash
CodeDirectory除了保存文件的hash值以外,还会保存另外七个特殊类型的hash:
CSSLOT_CODEDIRECTORY = 0, /* slot index for CodeDirectory */CSSLOT_INFOSLOT = 1,CSSLOT_REQUIREMENTS = 2,CSSLOT_RESOURCEDIR = 3,CSSLOT_APPLICATION = 4,CSSLOT_ENTITLEMENTS = 5,CSSLOT_DER_ENTITLEMENTS = 7,
CSSLOT_DER_ENTITLEMENTS是苹果公司在新版macos/ios中新加入的entitlements类型,用DER格式表示。

Entitlements是整个xml信息的hash值,使用ikit的--ent参数可以提取出所有的entitlements列表。
./ikit --ent -v /Applications/Safari.app/Contents/MacOS/Safari >safari.ent
3 内核态流程
3.1 简介
IOS的代码签名过程分为启动前、运行时、退出时。
3.1.1 进程启动前验签过程
签名的解析发生在macho从磁盘加载到内核的过程中。
bsd/kern/mach_loader.cparse_machfile(struct vnode *vp,vm_map_t map,thread_t thread,struct mach_header *header,off_t file_offset,off_t macho_size,int depth,int64_t aslr_offset,int64_t dyld_aslr_offset,load_result_t *result,load_result_t *binresult,struct image_params *imgp){if (os_add_overflow(mach_header_sz, header->sizeofcmds, &cmds_size) ||(off_t)cmds_size > macho_size ||round_page_overflow(cmds_size, &alloc_size) ||alloc_size > INT_MAX) {return LOAD_BADMACHO;}addr = kalloc_data(alloc_size, Z_WAITOK);if (addr == NULL) {return LOAD_NOSPACE;}error = vn_rdwr(UIO_READ, vp, addr, (int)alloc_size, file_offset,UIO_SYSSPACE, 0, vfs_context_ucred(imgp->ip_vfs_context), &resid, p);if (error) {kfree_data(addr, alloc_size);return LOAD_IOERROR;}
请注意xnu一次性将command全部从磁盘读入内存, text段等都包含在其内,这为后面校验hash提供了方便。
...case LC_CODE_SIGNATURE:ret = load_code_signature((struct linkedit_data_command *) lcp,vp,file_offset,macho_size,header->cputype,header->cpusubtype,result,imgp);while (off < alloc_size && ret == LOAD_SUCCESS) {tainted = CS_VALIDATE_TAINTED;valid = cs_validate_range(vp,NULL,ile_offset + off,(const void *)((uintptr_t)addr + off),MIN(PAGE_SIZE, cmds_size),&tainted);}}
load_code_signature用来解析签名信息。
staticload_return_tload_code_signature(struct linkedit_data_command *lcp,struct vnode *vp,off_t macho_offset,off_t macho_size,cpu_type_t cputype,cpu_subtype_t cpusubtype,load_result_t *result,struct image_params *imgp){blob = ubc_cs_blob_get(vp, cputype, cpusubtype, macho_offset);if (blob != NULL) {return;}
首先从文件vnode节点中提取出一个struct cs_blob结构,它保存的是签名信息,如果这个签名信息已经存在,则直接返回成功。
kr = ubc_cs_blob_allocate(&addr, &blob_size);if (kr != KERN_SUCCESS) {ret = LOAD_NOSPACE;goto out;}resid = 0;error = vn_rdwr(UIO_READ,vp,(caddr_t) addr,lcp->datasize,macho_offset + lcp->dataoff,UIO_SYSSPACE,0,kauth_cred_get(),&resid,current_proc());
如果不存在则调用ubc_cs_blob_allocate分配一块内存,调用vn_rdwr把签名信息读取到新分配的内存里。
if (ubc_cs_blob_add(vp,ip_platform,cputype,cpusubtype,macho_offset,&addr,datasize,imgp,0,&blob))
调用ubc_cs_blob_add把csblob加入到链表中。
intubc_cs_blob_add(struct vnode *vp,uint32_t platform,cpu_type_t cputype,cpu_subtype_t cpusubtype,off_t base_offset,vm_address_t *addr,vm_size_t size,struct image_params *imgp,__unused int flags,struct cs_blob **ret_blob){error = cs_blob_init_validated(addr, size, &tmp_blob, &cd);
首先验证签名信息的格式是否正确,这里的校验并不完全严格,请读者自行阅读。
blob_ro = zalloc_ro(ZONE_ID_CS_BLOB, Z_WAITOK | Z_NOFAIL);tmp_blob.csb_ro_addr = blob_ro;tmp_blob.csb_vnode = vp;/* AMFI needs to see the current blob state at the RO address. */zalloc_ro_update_elem(ZONE_ID_CS_BLOB, blob_ro, &tmp_blob);
注意csblob结构体使用zlloc的接口将其信息通过PPL保存在只读内存区,请参考后面关于PPL的分析部分。
error = mac_vnode_check_signature(vp, &tmp_blob, imgp, &cs_flags, &signer_type, flags, platform);}
mac_vnode_check_signature继续调用AMFI内核模块的接口,苹果公司的签名核心技术都存放于这个内核模块中,由于不会开源,我们将在下一章使用逆向过程技术还原出它所有的核心逻辑。
当load_code_signature返回成功后,则会调用cs_validate_range对所有的文件hash做对比测试,确保当前计算出的hash与文件加载进来的hash匹配。
注意:即使vnode中已经存在csblob信息,它只是不在走AMFI的流程,对所有文件的hash对比是每次都要进行的!因为保存在磁盘的时间,应用程序可能已经被窜改。
3.1.2 运行时验签过程
进程在启动时ios已经做了一次签名验证,但是在进程运行的过程中,代码和数据还是可能被篡改掉的,那么如何在进程的运行过程中进行签名的验证呢?它的验签逻辑可以发生在进程调度、系统调用过程、page fault甚至可以开一个单独的线程每隔一定时间进行轮询验签,但是作为一个工业级的os,需要考虑性能功耗的问题。ios选择了在page fault中做代码的hash值对比操作。
引起Page fault的原因有很多种,有发生在内核空间中的,有发生在用户空间中的,比如应用程序越界访问内核、应用程序对只读内存进行写操作,还有应用程序访问自身一段还没有被映射的页面,这个错误可以引发Copy on write动作或者这个页面之前被交换到磁盘上,那么在页面换回时,势必要再进行一次签名验证。
3.1.2.1 权限错误处理流程
在进行异常处理前,需要先判断一下引起这个fault的物理页是否需要做一次签名验证,因为如果这块物理页已经被污染过,那么起始没必要进行fault处理了。如果没被污染, ios也有一些优化可以快速避开本次验签过程。vm_fault_cs_check_violation函数就是用来做这些事情。
vm_fault_cs_check_violation(bool cs_bypass,vm_object_t object,vm_page_t m,pmap_t pmap,vm_prot_t prot,vm_prot_t caller_prot,vm_map_size_t fault_page_size,vm_map_offset_t fault_phys_offset,vm_object_fault_info_t fault_info,bool map_is_switched,bool map_is_switch_protected,bool *cs_violation){if (!cs_bypass &&vm_fault_cs_need_validation(pmap, m, object,fault_page_size, fault_phys_offset)) {vm_object_lock_assert_exclusive(object);if (VMP_CS_VALIDATED(m, fault_page_size, fault_phys_offset)) {vm_cs_revalidates++;}vm_page_validate_cs(m, fault_page_size, fault_phys_offset);}}
vm_fault_cs_need_validation用来判断这个物理页是否需要进行验签过程。
staticboolvm_fault_cs_need_validation(pmap_t pmap,vm_page_t page,vm_object_t page_obj,vm_map_size_t fault_page_size,vm_map_offset_t fault_phys_offset){if (pmap == kernel_pmap) {/* 1 - not user space */return false;}
验签只针对用户态代码和数据。
if (!page_obj->code_signed) {/* 3 - page does not belong to a code-signed object */return false;}if (fault_page_size == PAGE_SIZE) {/* looking at the whole page */if (page->vmp_cs_tainted == VMP_CS_ALL_TRUE) {/* 2 - page is all tainted */return false;}
如果这个物理页已经被污染掉,此次就不在需要再判断了。
if (page->vmp_cs_validated == VMP_CS_ALL_TRUE &&!page->vmp_wpmapped) {/* 4 - already fully validated and never mapped writable */return false;}
如果这个物理页此前已经被处理过,那么此次fault也不会再判断了。Ios的jit签名就是利用了这一点绕过了签名的验证。
} else {/* looking at a specific sub-page */if (VMP_CS_TAINTED(page, fault_page_size, fault_phys_offset)) {/* 2 - sub-page was already marked as tainted */return false;}if (VMP_CS_VALIDATED(page, fault_page_size, fault_phys_offset) &&!page->vmp_wpmapped) {/* 4 - already validated and never mapped writable */return false;}}/* page needs to be validated */return true;}voidvm_page_validate_cs(vm_page_t page,vm_map_size_t fault_page_size,vm_map_offset_t fault_phys_offset){vm_object_t object;object = VM_PAGE_OBJECT(page);vm_object_lock_assert_held(object);if (vm_page_validate_cs_fast(page, fault_page_size, fault_phys_offset)) {return;}vm_page_map_and_validate_cs(object, page);}staticboolean_tvm_page_validate_cs_fast(vm_page_t page,vm_map_size_t fault_page_size,vm_map_offset_t fault_phys_offset){vm_object_t object;object = VM_PAGE_OBJECT(page);vm_object_lock_assert_held(object);if (page->vmp_wpmapped&&!VMP_CS_TAINTED(page, fault_page_size, fault_phys_offset)) {/** This page was mapped for "write" access sometime in the* past and could still be modifiable in the future.* Consider it tainted.* [ If the page was already found to be "tainted", no* need to re-validate. ]*/vm_object_lock_assert_exclusive(object);VMP_CS_SET_VALIDATED(page, fault_page_size, fault_phys_offset, TRUE);VMP_CS_SET_TAINTED(page, fault_page_size, fault_phys_offset, TRUE);if (cs_debug) {printf("CODESIGNING: %s: ""page %p obj %p off 0x%llx ""was modified\n",__FUNCTION__,page, object, page->vmp_offset);}vm_cs_validated_dirtied++;}if (VMP_CS_VALIDATED(page, fault_page_size, fault_phys_offset) ||VMP_CS_TAINTED(page, fault_page_size, fault_phys_offset)) {return TRUE;}}
vm_page_validate_cs_fast有个特殊的处理,如果page->vmp_wpmapped被设置,说明这个物理页在之前的某段时间里被设置为可写状态,xnu认为在将来的一段时间这个可写状态会一直保持,所以直接将这个物理页标记为污染和审计过,直接返回。
vm_page_map_and_validate_cs函数则对这个物理页做hash对比操作,读者可自行阅读相关代码。
3.1.2.2 磁盘交换处理流程
接下来就要分析页面从磁盘交换回内存时的签名验证过程:
Osfmk/vm/vm_fault.cvm_fault_page->memory_object_data_requestOsfmk/vm/memory_object.ckern_return_tmemory_object_data_request(memory_object_t memory_object,memory_object_offset_t offset,memory_object_cluster_size_t length,vm_prot_t desired_access,memory_object_fault_info_t fault_info){return (memory_object->mo_pager_ops->memory_object_data_request)(memory_object,offset,length,desired_access,fault_info);}
xnu提供了几种不同的内存后备存储方式,比如swap交换文件、普通文件、内存压缩、 apple特有的内存加密方式,这些后备存储都对应不同的分页器,但只有几种后备存储才会在内存交换回来时做签名验证, 这里我们只以4k形式的分页器进行分析。
Osfmk/vm/vm_fourk_pager.cconst struct memory_object_pager_ops fourk_pager_ops = {.memory_object_reference = fourk_pager_reference,.memory_object_deallocate = fourk_pager_deallocate,.memory_object_init = fourk_pager_init,.memory_object_terminate = fourk_pager_terminate,.memory_object_data_request = fourk_pager_data_request,.memory_object_data_return = fourk_pager_data_return,.memory_object_data_initialize = fourk_pager_data_initialize,.memory_object_data_unlock = fourk_pager_data_unlock,.memory_object_synchronize = fourk_pager_synchronize,.memory_object_map = fourk_pager_map,.memory_object_last_unmap = fourk_pager_last_unmap,.memory_object_data_reclaim = NULL,.memory_object_backing_object = NULL,.memory_object_pager_name = "fourk_pager"};fourk_pager_data_request(memory_object_t mem_obj,memory_object_offset_t offset,memory_object_cluster_size_t length,vm_prot_t protection_required,memory_object_fault_info_t mo_fault_info){subpg_validated = FALSE;subpg_tainted = 0;if (src_page_object->code_signed) {vm_page_validate_cs_mapped_chunk(src_page,(const void *) src_vaddr,offset_in_src_page,FOURK_PAGE_SIZE,&subpg_validated,&subpg_tainted);num_subpg_signed++;if (subpg_validated) {num_subpg_validated++;}if (subpg_tainted & CS_VALIDATE_TAINTED) {num_subpg_tainted++;}if (subpg_tainted & CS_VALIDATE_NX) {/* subpg should not be executable */if (sub_page_cnt > 1) {} else {num_subpg_nx++;}}}
vm_page_validate_cs_mapped_chunk对一块物理内存进行签名验证,因为在页面被交换到磁盘的这段时间内,可能会被篡改掉。
3.1.2.3 页面复制时
此外在进行一个物理页面的复制时,也需要对源物理页进行签名验证。
osfmk\vm\vm_resident.cvoidvm_page_copy(vm_page_t src_m,vm_page_t dest_m){vm_object_t src_m_object;src_m_object = VM_PAGE_OBJECT(src_m);vm_object_lock_assert_held(src_m_object);if (src_m_object != VM_OBJECT_NULL &&src_m_object->code_signed) {vm_page_copy_cs_validations++;vm_page_validate_cs(src_m, PAGE_SIZE, 0);}dest_m->vmp_cs_tainted = src_m->vmp_cs_tainted;dest_m->vmp_cs_nx = src_m->vmp_cs_nx;if (dest_m->vmp_cs_tainted) {vm_page_copy_cs_tainted++;}dest_m->vmp_error = src_m->vmp_error; /* sliding src_m might have failed... */pmap_copy_page(VM_PAGE_GET_PHYS_PAGE(src_m), VM_PAGE_GET_PHYS_PAGE(dest_m));}
Src_m代表源页面,如果它对应的object对象有code_signed标志,那么就会进行一次签名验证,页面拷贝需要确保它的完整性。
3.1.2.4 只读rootfs的优化
对于只读rootfs上的文件,xnu做了优化,在page fault第一次验证完签名后,如果当前进程没有试图将只读rootfs挂载为可写,那么后续就不再做签名验证,属于性能优化的一部分。
Bsd/sys/ubc_internal.h
struct ubc_info {struct cs_blob * cs_blobs; /* for CODE SIGNING */struct cs_blob * cs_blob_supplement;/* supplemental blob (note that there can only be one supplement) */void * XNU_PTRAUTH_SIGNED_PTR("ubc_info.cs_valid_bitmap") cs_valid_bitmap; /* right now: used only for signed files on the read-only root volume */uint64_t cs_valid_bitmap_size;}
Ubc_info结构体除了保存struct cs_blob结构体外,还可选的保存了一个bitmap,它用来设置一个物理页的签名状态。
Osfmk/vm/vm_protos.h
#define CS_BITMAP_SET1//对页面设置为已经检查过
#define CS_BITMAP_CLEAR 2// 清除签名状态
#define CS_BITMAP_CHECK 3// 对页面进行签名检查
vm_page_validate_cs_fast函数前面讲过它会进行预判此次page fault是否需要验证这个页面的签名。
staticboolean_tvm_page_validate_cs_fast(vm_page_t page,vm_map_size_t fault_page_size,vm_map_offset_t fault_phys_offset){kern_return_t kr;kr = vnode_pager_cs_check_validation_bitmap(object->pager,page->vmp_offset + object->paging_offset,CS_BITMAP_CHECK);if (kr == KERN_SUCCESS) {page->vmp_cs_validated = VMP_CS_ALL_TRUE;page->vmp_cs_tainted = VMP_CS_ALL_FALSE;vm_cs_bitmap_validated++;return TRUE;}
它会调用vnode_pager_cs_check_validation_bitmap传递CS_BITMAP_CHECK进行判断是否需要签名验证。
vnode_pager_cs_check_validation_bitmap进一步调用ubc_cs_check_validation_bitmap。
kern_return_tubc_cs_check_validation_bitmap(vnode_t vp,memory_object_offset_t offset,int optype){kern_return_t kr = KERN_SUCCESS;if (!USE_CODE_SIGN_BITMAP(vp)|| !UBCINFOEXISTS(vp)) { 【1】kr = KERN_INVALID_ARGUMENT;} else {struct ubc_info *uip = vp->v_ubcinfo;char *target_bitmap = uip->cs_valid_bitmap;if (target_bitmap == NULL) {kr = KERN_INVALID_ARGUMENT;} else {uint64_t bit, byte;bit = atop_64( offset );byte = bit >> 3;if (byte > uip->cs_valid_bitmap_size) {kr = KERN_INVALID_ARGUMENT;} else {if (optype == CS_BITMAP_SET) {target_bitmap[byte] |= (1 << (bit & 07));kr = KERN_SUCCESS;} else if (optype == CS_BITMAP_CLEAR) {target_bitmap[byte] &= ~(1 << (bit & 07));kr = KERN_SUCCESS;} else if (optype == CS_BITMAP_CHECK) {if (target_bitmap[byte] & (1 << (bit & 07))) { 【2】kr = KERN_SUCCESS;} else {kr = KERN_FAILURE;}}}}}return kr;}
#define USE_CODE_SIGN_BITMAP(vp) ( (vp != NULL) && (vp->v_mount != NULL) && (vp->v_mount->mnt_flag & MNT_ROOTFS) && !root_fs_upgrade_try)USE_CODE_SIGN_BITMAP这个首先宏判断vnode是不是rootfs,并且要判断root_fs_upgrade_try是否为0。这个变量代表的是进程试图将只读的文件系统挂载为可写。
boolean_t root_fs_upgrade_try = FALSE;int__mac_mount(struct proc *p, register struct __mac_mount_args *uap, __unused int32_t *retval){if ((flags & MNT_RDONLY) == 0) {root_fs_upgrade_try = TRUE;}}
所以【1】处的逻辑为:如果vnode属于rootfs,并且root_fs_upgrade_try设置为了1,说明有进程将只读rootfs重新挂载为可写,那么就必须对这个物理页进行签名验证,否则在【2】处要进行一次页面曾经是否被检测过的判断,如果对应的比特位已经被设置,则无需进行签名验证,否则需要进行签名验证。我们看到CS_BITMAP_CHECK是用来判断比特位是否设置的,那么CS_BITMAP_SET是在什么时候调用的呢?答案在vm_page_validate_cs_mapped_slow中。
voidvm_page_validate_cs_mapped_slow(vm_page_t page,const void *kaddr){vm_object_t object;memory_object_offset_t mo_offset;memory_object_t pager;struct vnode *vnode;int validated, tainted, nx;assert(page->vmp_busy);object = VM_PAGE_OBJECT(page);vm_object_lock_assert_exclusive(object);vm_cs_validates++;pager = object->pager;assert(object->paging_in_progress);vnode = vnode_pager_lookup_vnode(pager);mo_offset = page->vmp_offset + object->paging_offset;/* verify the SHA1 hash for this page */validated = 0;tainted = 0;nx = 0;cs_validate_page(vnode,pager,mo_offset,(const void *)((const char *)kaddr),&validated,&tainted,&nx);page->vmp_cs_validated |= validated;page->vmp_cs_tainted |= tainted;page->vmp_cs_nx |= nx;if (page->vmp_cs_validated == VMP_CS_ALL_TRUE &&page->vmp_cs_tainted == VMP_CS_ALL_FALSE) {vnode_pager_cs_check_validation_bitmap(object->pager,mo_offset,CS_BITMAP_SET);}}
cs_validate_page对物理页进行hash值验证,如果合法,则调用vnode_pager_cs_check_validation_bitmap传递CS_BITMAP_SET对相应比特位进行了设置。此后ubc_cs_check_validation_bitmap发现如果页面比特位已经被设置,后面就无需再进行签名验证了。
3.1.3 进程退出时签名验证
在进程正常退出时,xnu是不会做签名验证的,只有在异常退出时,且有以下几种情况就会必须验证签名:当进程收到coredump或crash report信号时,或者因进程签名验证失败收到CS_KILLED信号时。
Bsd/kern/kern_exit.c
voidproc_prepareexit(proc_t p, int rv, boolean_t perf_notify){/** Generate a corefile/crashlog if:* The process doesn't have an exit reason that indicates no crash report should be created* AND any of the following are true:* - The process was terminated due to a fatal signal that generates a core* - The process was killed due to a code signing violation* - The process has an exit reason that indicates we should generate a crash report** The first condition is necessary because abort_with_reason()/payload() use SIGABRT* (which normally triggers a core) but may indicate that no crash report should be created.*/if (!(PROC_HAS_EXITREASON(p) && (PROC_EXITREASON_FLAGS(p) & OS_REASON_FLAG_NO_CRASH_REPORT)) &&(hassigprop(WTERMSIG(rv), SA_CORE) || ((proc_getcsflags(p) & CS_KILLED) != 0) ||(PROC_HAS_EXITREASON(p) && (PROC_EXITREASON_FLAGS(p) &OS_REASON_FLAG_GENERATE_CRASH_REPORT)))) {task_t task = proc_task(p);uintptr_t bt[2];struct backtrace_user_info btinfo = BTUINFO_INIT;unsigned int frame_count = backtrace_user(bt, 2, NULL, &btinfo); [1]int bt_err = btinfo.btui_error;if (bt_err == 0 && frame_count >= 1) {/** First check at the page containing the current PC.* This passes if the page code signs -or- if we can't figure out* what is at that address. The latter action is so we continue checking* previous pages which may be corrupt and caused a wild branch.*/kr = revalidate_text_page(task,bt[0]); [2]/* No corruption found, check the previous sequential page */if (kr == KERN_SUCCESS) {kr = revalidate_text_page(task,bt[0] - get_task_page_size(task)); [3]}/* Still no corruption found, check the current function's caller */if (kr == KERN_SUCCESS) {if (frame_count > 1 &&atop(bt[0]) != atop(bt[1]) && /* don't recheck PC page */atop(bt[0]) - 1 != atop(bt[1])) { /* don't recheck page before */kr = revalidate_text_page(task, (vm_map_offset_t)bt[1]); [4]}}if (kr != KERN_SUCCESS) {os_log(OS_LOG_DEFAULT,"Text page corruption detected in dying process %d\n", proc_getpid(p));}}}
在[1]处先获取当前函数栈的backtrace信息,bt[0]代表当前的PC指针,bt[1]代表当前函数caller的地址。在[2]处调用revalidate_text_page对当前pc所在的物理页进行一次签名检查,如果没有问题,则在[3]处对当前pc地址所在的前一个物理页进行签名验证,没有还是没有问题,就在对函数的caller地址所在的物理页进行签名检测。我们看到这一系列操作只是试图检测当前pc附近的代码段有没有被破坏。
Osfmk/vm/vm_fault.ckern_return_trevalidate_text_page(task_t task, vm_map_offset_t code_addr){kern_return_t kr;vm_map_t map;vm_object_t object = NULL;vm_object_offset_t offset;vm_page_t page = NULL;struct vnode *vnode;uint64_t *diagnose_buffer = NULL;CA_EVENT_TYPE(vmtc_telemetry) * event = NULL;ca_event_t ca_event = NULL;map = task->map;if (task->map == NULL) {return KERN_SUCCESS;}kr = vmtc_revalidate_lookup(map, code_addr, &object, &offset, &page); [1]if (kr != KERN_SUCCESS) {goto done;}/** The object needs to have a pager.*/if (object->pager == NULL) {goto done;}/** Needs to be a vnode backed page to have a signature.*/vnode = vnode_pager_lookup_vnode(object->pager);if (vnode == NULL) {goto done;}/** Object checks to see if we should proceed.*/if (!object->code_signed || /* no code signature to check */object->internal || /* internal objects aren't signed */object->terminating || /* the object and its pages are already going away */!object->pager_ready) { /* this should happen, but check shouldn't hurt */goto done;}/** Check the code signature of the page in question.*/vm_page_map_and_validate_cs(object, page); [2]/** At this point:* vmp_cs_validated |= validated (set if a code signature exists)* vmp_cs_tainted |= tainted (set if code signature violation)* vmp_cs_nx |= nx; ??** if vmp_pmapped then have to pmap_disconnect..* other flags to check on object or page?*/if (page->vmp_cs_tainted != VMP_CS_ALL_FALSE) { [3]/** On development builds, a boot-arg can be used to cause* a panic, instead of a quiet repair.*/if (vmtc_panic_instead) {panic("Text page corruption detected: vm_page_t 0x%llx", (long long)(uintptr_t)page);}/** We're going to invalidate this page. Grab a copy of it for comparison.*/ca_event = CA_EVENT_ALLOCATE(vmtc_telemetry);event = ca_event->data;diagnose_buffer = vmtc_text_page_diagnose_setup(code_addr, page, event);/** Invalidate, i.e. toss, the corrupted page.*/if (!page->vmp_cleaning &&!page->vmp_laundry &&!page->vmp_fictitious &&!page->vmp_precious &&!page->vmp_absent &&!page->vmp_error &&!page->vmp_dirty &&!is_page_wired(page)) {if (page->vmp_pmapped) {int refmod = pmap_disconnect(VM_PAGE_GET_PHYS_PAGE(page));if (refmod & VM_MEM_MODIFIED) {SET_PAGE_DIRTY(page, FALSE);}if (refmod & VM_MEM_REFERENCED) {page->vmp_reference = TRUE;}}/* If the page seems intentionally modified, don't trash it. */if (!page->vmp_dirty) {VM_PAGE_FREE(page);} else {event->vmtc_not_eligible = true;}} else {event->vmtc_not_eligible = true;}vm_object_unlock(object);object = VM_OBJECT_NULL;/** Now try to diagnose the type of failure by faulting* in a new copy and diff'ing it with what we saved.*/if (diagnose_buffer != NULL) {vmtc_text_page_diagnose(code_addr, diagnose_buffer, event);}if (corruption_test_va != 0) {corruption_test_va = 0;event->vmtc_testing = true;}kernel_triage_record(thread_tid(current_thread()),KDBG_TRIAGE_EVENTID(KDBG_TRIAGE_SUBSYS_VM, KDBG_TRIAGE_RESERVED, KDBG_TRIAGE_VM_TEXT_CORRUPTION),0 /* arg */);CA_EVENT_SEND(ca_event);printf("Text page corruption detected for pid %d\n", proc_selfpid());++vmtc_total;return KERN_FAILURE;}done:if (object != NULL) {vm_object_unlock(object);}return KERN_SUCCESS;}
在[1]处获取addr对应的object和page信息,在[2]处调用vm_page_map_and_validate_cs来对这个page进行签名验证,在[3]处如果检测到签名失败,还要进行详细的信息获取,比如要对比这个page是哪一块内存出现了问题。
3.2 AMFI处理流程
Amfi(Apple Mobile File Integrity)是一个内核扩展, IOS的代码验签的过程都集中在这个模块中,本文以ios14为例来剖析ios的代码验签过程。
3.2.1 vnode_check_signature
MAC系统通过以下代码进入到amfi流程中:
intmac_vnode_check_signature(struct vnode *vp, struct cs_blob *cs_blob,struct image_params *imgp,unsigned int *cs_flags, unsigned int *signer_type,int flags, unsigned int platform) {MAC_CHECK(vnode_check_signature, vp, vp->v_label, cpu_type, cs_blob,cs_flags, signer_type, flags, platform, &fatal_failure_desc, &fatal_failure_desc_len);}
__ZL22_vnode_check_signatureP5vnodeP5labeliP7cs_blobPjS5_ijPPcPmMOV W0, #1048 ; sizeBL _IOMalloc_externalCBZ X0, loc_FFFFFFF008308F44MOV X22, X0 ; unkown_struct;LDP X25, X26, [X29,#arg_0]ADD X0, X0, #8 ; __dstMOV X27, X22MOVK X27, #0x6712,LSL#48ADRL X8, off_FFFFFFF0077AE4F0PACDA X8, X27STR X8, [X22] ; *(unkown_struct + 0) = pacda()ADRL X1, l_.str.82 ; __srcMOV W2, #1024 ; __nBL _memmove ; memmove(unkown_struct + 8, src, 1024);STRB WZR, [X22,#1032] ; *(unkown_struct + 1032) = 0ADRL X8, off_FFFFFFF0077AE4C0PACDA X8, X27STR X8, [X22]STR X24, [X22,#1040] ; *(unkown_struct + 1040) = vnodeMOV X0, X22MOV X1, X23MOV X2, X21MOV X3, X20MOV X4, X19MOV W5, #0MOV X6, X25MOV X7, X26 ; (unkown_struct, csblob, cs_flags, signer_type, flags, 0, *addr1, *addr2)BL__ZN20StaticPlatformPolicyILb1ELb1ELb0ELb0ELb0ELb0ELb1ELb1ELb0ELb1ELb0ELb0ELb0ELb0ELb1ELb1ELj2ELb1ELb0EE15check_signatureEP8LazyPathiP7cs_blobPjS5_ibbPPcPm
进一步调用check_signature函数,从上面的代码中,我们可以推测它的参数结构为:
check_signature(unkown_struct, csblob, cs_flags, signer_type, flags, 0, *addr1, *addr2)。3.2.2 check_signature
3.2.2.1 cd hash在trustcache里
// (unkown_struct, csblob, cs_flags, signer_type, flags, 0, *addr1, *addr2)__int64 __fastcall StaticPlatformPolicy<true,true,false,false,false,false,true,true,false,true,false,false,false,false,true,true,2u,true,false>::check_signature(LazyPath *a1,__int64 a2,unsigned int *a3,_DWORD *a4,unsigned int a5,__int64 a6,char **a7,unsigned __int64 *a8){if ( (StaticPlatformPolicy<true,true,false,false,false,false,true,true,false,true,false,false,false,false,true,true,2u,true,false>::loadEntitlementsFromSignature(&inColl,a2,&v114) & 1) == 0 ){v30 = fatal_error_fmt(a1,a7,a8,"The signature could not be validated because AMFI could not load its entitlements for validation: %s",v114);v31 = 0LL;v32 = 1LL;v16 = inColl;if ( !inColl )goto LABEL_20;goto LABEL_19;}
loadEntitlementsFromSignature函数的作用是从struct cs_blob中获取进程的entitlements,它的大小在8byte到128m之间。如果cs_blob存储的cd hash在static trustcache或loaded trustcache中,那么会在entitlements中加入一个新节点platform-application,如果cs_blob存取的identifier为空,则在entitlements中删除节点com.apple.private.signing-identifier,否则设置为cs_blob对应的identifier。
...if ( v23 && (v24 = (const char *)((__int64 (*)(void))v23->__vftable[1].isEqualTo)()) != 0LL ){v25 = (OSDictionary *)v24;*a3 |= 0x200u; // cs_flags |= CS_KILLif ( !strcmp(v24, "get-task-allow") ){LOBYTE(v126[0]) = 0;if ( !(unsigned int)AppleMobileFileIntegrity::AMFIEntitlementGetBool((AppleMobileFileIntegrity *)v16,v25,(const char *)v126,v26)&& LOBYTE(v126[0]) != 0 ){*a3 |= 4u; // CS_GET_TASK_ALLOW}}}...
获取entitlements后,首先将第三个参数cs_flags加上CS_KILL标志。如果entitlements包含get-task-allow节点,则再把CS_GET_TASK_ALLOW标志加入到cs_flags中。
cdhash = (const unsigned __int8 *)csblob_get_cdhash(v13);if ( !cdhash ){v30 = fatal_error_fmt(a1, a7, a8, "Internal Error: No cdhash found.");goto LABEL_37;}
接着获取进程的cd hash,如果不存在直接返回。
if ( (AMFIIsCodeDirectoryInTrustCache(cdhash) & 1) == 0 && !(unsigned int)codeDirectoryHashIsInLoadedTrustCache(v37) ){}
如果cd hash在static trustcache或loaded trustcache里。
v39 = postValidation(a1, v13, v38, "in-kernel", a7, a8);v30 = IsCDHashBlacklisted(v37);if ( (_DWORD)v30 ){v30 = fatal_error_fmt(a1, a7, a8, "Bad things happened.");LABEL_37:v31 = 0LL;goto LABEL_82;}
postValidation函数判断cs_flags是否包含CS_PLATFORM_BINARY,如果是返回1。否则继续判断hash type是否为sha256,如果是返回1,其余返回0。
Ios还为cd hash设置了一个黑名单,IsCDHashBlacklisted函数判断一个cd hash是否在黑名单中。
*a3 |= 0x20000000u;// CS_SIGNED
最后将CS_SIGNED标志放入到cs_flags中,函数返回。
我们看到如果一个cd hash在static trustcache或者在loaded trustcache里,则直接完成此次判断,接下来继续分析不在trustcache里的情况。
3.2.2.2 cd hash不在trustcache里
if ( _codeDirectoryHashInCompilationServiceHash(v37) && (unsigned int)noEntitlementsPresent(v13) ){LOBYTE(v126[0]) = 0;v41 = current_proc();AppleMobileFileIntegrity::AMFIEntitlementGetBool(v41,(proc *)"com.apple.private.amfi.can-execute-cdhash",(const char *)v126,v42);if ( LOBYTE(v126[0]) ){v30 = postValidation(a1, v13, *a3, "can-execute-cdhash", a7, a8);v40 = v30 ^ 1; // 非(platform binary并且hash type为sha256)}else{IOLog("AMFI: can-execute-cdhash code in non-entitled context.\n");v40 = 1;}v31 = 0LL;goto LABEL_81;}
首先判断这个cd hash如果在compilationServiceHash里,并且进程所含的entitlements不为空,如果进程的entitlements包含com.apple.private.amfi.can-execute-cdhash,并且这个进程并非platform binary,则完成此次判断直接返回。compilationServiceHash由用户态进程调用AMFI loadCompilationServiceCodeDirectoryHash接口进行设置,请参考3.3节。
addr = (unsigned int *)csblob_get_addr(v112);size = csblob_get_size(v112);blob_bytes = csblob_find_blob_bytes(addr, size, 0, 0xFADE0C02);// CSMAGIC_CODEDIRECTORYif ( !blob_bytes ){printf("AMFI: '%s' has no cd?\n", v48);v123[1] = 0LL;LABEL_63:printf("AMFI: '%s': Unrecoverable CT signature issue, bailing out.\n");goto LABEL_79;}
接着搜索cs_blob结构是否包含CSMAGIC_CODEDIRECTORY,如果不存在则直接返回。
v52 = (__int64)blob_bytes;v108 = a1;v53 = bswap32(blob_bytes[1]); // CS_GenericBlob.lengthif ( v53 <= 7 ){printf("AMFI: '%s' has short cd (%u)?\n");LABEL_62:v123[1] = 0LL;goto LABEL_63;}
CODEDIRECTORY结构不能小于7。
v54 = (unsigned int *)csblob_get_addr(v112);v55 = csblob_get_size(v112);v56 = csblob_find_blob_bytes(v54, v55, 0x10000, 0xFADE0B01);// CSMAGIC_BLOBWRAPPER CMS Signatureif ( !v56 ){printf("AMFI: '%s' has no CMS blob?\n");goto LABEL_62;}
继续搜索cs_blob是否包含CSMAGIC_BLOBWRAPPER,它会携带证书信息。
if ( v58 <= 7 ) // CS_GenericBlob.length{printf("AMFI: '%s' has short cms (%u), bailing out.\n");goto LABEL_62;}
Cms结构不能小于7。
if ( v57 == 0x8000000 ) // CS_GenericBlob.length == 0{if ( (unsigned int)CTEvaluateAMFICodeSignatureCMS(v59,v100,v52,v53,v60,0LL,0LL,(__int64 *)&v123[1],&v117,&v116,v123,(__int64)&v122) ){v123[1] = 0LL;printf("AMFI: '%s' does not pass CT evaluation, result: %#x\n");goto LABEL_63;}}
CTEvaluateAMFICodeSignatureCMS函数验证cms携带的证书是否由苹果公司签发。
if ( (_DWORD)v116 ){hashtype = csblob_get_hashtype(v112);v70 = (const unsigned __int8 *)v123[0];if ( (_DWORD)v116 == 1 ){v71 = 1;v61 = v108;}else{v61 = v108;if ( (_DWORD)v116 != 4 ){printf("AMFI: '%s' has unknown CT digest type %#x\n");goto LABEL_149;}v71 = 2;}if ( v71 == hashtype ){if ( v122 > 19 ){if ( !memcmp(v46, v123[0], 0x14uLL) )goto LABEL_70;*(_QWORD *)&v94 = 0xAAAAAAAAAAAAAAAALL;*((_QWORD *)&v94 + 1) = 0xAAAAAAAAAAAAAAAALL;*(_OWORD *)&v125[25] = v94;*(_OWORD *)v125 = v94;*(_OWORD *)&v125[16] = v94;*(_OWORD *)&v127[9] = v94;*(_OWORD *)v126 = v94;*(_OWORD *)v127 = v94;cdhash_to_string(v125, v46);cdhash_to_string((char *)v126, v70);printf("AMFI: '%s': cdhash mismatch: actual '%s' != expected '%s'\n");}else{printf("AMFI: '%s' has unexpected digest data len %zu (type %#x)\n");}}else{printf("AMFI: '%s' has unexpected digest type, actual %#x != expected %#x\n");}LABEL_149:printf("AMFI: '%s': V2 hash agility validation failed, bailing out.\n");goto LABEL_63;}
如果v116不为空,还要判断CTEvaluateAMFICodeSignatureCMS返回的cd hash值是否与当前csblob的cd hash值是否相同,不相同则验证失败返回。
LABEL_70:if ( ((unsigned __int64)v123[1] & 0x1FF80) == 0 ){printf("AMFI: '%s': unsuitable CT policy %#llx for this platform/device, rejecting signature.\n");goto LABEL_79;}
v123[1]是CTEvaluateAMFICodeSignatureCMS返回的某个标志值。
if ( ((__int64)v123[1] & 0x5100) != 0 && allow3rdParty && !IsCDHashBlacklisted(v46)){IOLog("App Store Fast Path -> %s\n", v48);v64 = *a3 | 0x4000; // CS_ENTITLEMENTS_VALIDATED*a3 = v64;v15 = v107;v65 = v103;v63 = v105;goto LABEL_123;}*a3 |= 0x20000000u; // CS_SIGNED
0x5100代表app由应用市场签发,对cs_flags标志设置 CS_ENTITLEMENTS_VALIDATED|CS_SIGNED,然后直接返回。
可以看到ios对app应用也可以使能CDHashBlacklisted,这样可以将某个app的cd hash加入黑名单,就可以防止它运行。
if ( !allow3rdParty){printf("AMFI: '%s' does not pass kernel-side policy for this model, bailing out.\n", v48);v62 = 0;v66 = 0;v63 = v105;LABEL_120:if ( !(v66 | v62) ){if ( v102 && !v113 ){printf("AMFI: code signature validation failed.\n");printf("AMFI: bailing out because of restricted entitlements.\n");v30 = fatal_error_fmt(v61,v105,v103,"Code has restricted entitlements, but the validation of its code signature failed.\n""Unsatisfied Entitlements: %s",v31);goto LABEL_80;}LABEL_79:v30 = printf("AMFI: code signature validation failed.\n");LABEL_80:v40 = 1;v15 = v107;goto LABEL_81;}if ( !v66 ){v30 = printf("AMFI: code signature validation failed.\n");v15 = v107;if ( v62 ){IOLog("AMFI: detected an anomaly during entitlement parsing.\n");v30 = fatal_error_fmt(v61, v63, v103, "anomaly detecting during entitlement parsing\n");}v40 = 1;goto LABEL_81;}v64 = *a3;v15 = v107;v65 = v103;LABEL_123:v30 = postValidation(v61, v112, v64, "dynamic", v63, v65);v40 = v30 ^ 1;goto LABEL_81;}
如果内核配置为不允许加载三方应用,则打印一些出错信息,直接返回。
if ( (csblob_get_flags(v112) & 2) == 0 ) // CS_ADHOC{printf("AMFI: '%s' has zero length cms, but is not adhoc?\n");goto LABEL_63;}printf("AMFI: '%s' is adhoc signed.\n", v48);v61 = v108;
如果允许三方应用,则判断cs_flags是否为CS_ADHOC,不是的话则直接返回。
接下来要与用户态进程amfid交互,来判断CS_ADHOC进程是否合法。
if ( (unsigned int)getDaemonPort(v123) ){IOLog("StaticPlatformPolicy<%d>: no registered daemon port\n", 2LL);v62 = 0;v63 = v105;LABEL_116:v86 = 0;LABEL_117:if ( v62 )v66 = 0;elsev66 = v86;goto LABEL_120;}
获取afmid进程的port,mach微内核通过port进行进程间通讯。
v91 = verify_code_directory((__int64)v89,(__int64)v61 + 8,base_offset,v110,v88,v113 != 0,2LL,v116,v117,(_DWORD *)&v118 + 1,&v120,&v119,v109,(_DWORD *)&v121 + 1,&v121,&v118,v31,(__int64)&v123[1],v126);
调用verify_code_directory让用户态amfid去做校验。
if ( v92 == -308 ){IOLog("StaticPlatformPolicy<%d>: verify_code_directory server is dead\n");}else if ( v92 ){IOLog("StaticPlatformPolicy<%d>: verify_code_directory returned 0x%x\n");}
当用户态amfid进程返回出错码,则直接返回。
*(_OWORD *)v125 = *(_OWORD *)v126;*(_OWORD *)&v125[16] = *(_OWORD *)v127;if ( (unsigned int)tokenIsTrusted((audit_token_t *)v125) ){v62 = v118 != 0;if ( v120 == 1 ){if ( !memcmp(v68, &v123[1], 20uLL) ){if ( HIDWORD(v121) )panic("\"amfid is broken. (%s) (%d) (%d) (%d)\"", (const char *)v61 + 8, v88, v120, HIDWORD(v121));if ( (_DWORD)v121 )*a3 |= 0x40000000u; // CS_DEV_CODEif ( v119 )*a3 &= ~0x800u; // CS_RESTRICTv87 = BYTE4(v118);if ( HIDWORD(v118) ){v87 = 0;*a3 |= 0x4200u; // CS_KILL|CS_ENTITLEMENTS_VALIDATED}v86 = 1;LABEL_115:v63 = v105;if ( (v87 & 1) == 0 )goto LABEL_117;goto LABEL_116;}IOLog("%s: Possible race detected. Rejecting.\n", (const char *)v61 + 8);IOLog("StaticPlatformPolicy<%d>: Unable to match identity\n", 2LL);
当用户态afmid返回成功时,会先调用tokenIsTrusted传递当前进程的audit_token,它根据audit_token找到当前进程的cd hash是否在static trustcache中,以此证明当前内核确实是与amfid进程通讯,同时amfid会返回给内核一个cd hash值,如果与当前进程的cd hash值相同,则验证通过。根据不同的返回值设置相应的cs_flags,然后返回。
3.2.3 额外的几个服务函数
AppleMobileFileIntegrityUserClient::externalMethod(unsigned int, IOExternalMethodArguments *, IOExternalMethodDispatch *, OSObject *, void *)::methodsDCQ __ZN34AppleMobileFileIntegrityUserClient14loadTrustCacheEP8OSObjectPvP25IOExternalMethodArgumentsDCQ __ZN34AppleMobileFileIntegrityUserClient19flushAllValidationsEP8OSObjectPvP25IOExternalMethodArgumentsDCQ __ZN34AppleMobileFileIntegrityUserClient39loadCompilationServiceCodeDirectoryHashEP8OSObjectPvP25IOExternalMethodArgumentsDCQ __ZN34AppleMobileFileIntegrityUserClient20isCdhashInTrustCacheEP8OSObjectPvP25IOExternalMethodArgumentsDCQ __ZN34AppleMobileFileIntegrityUserClient14loadTrustCacheEP8OSObjectPvP25IOExternalMethodArgumentsDCQ __ZN34AppleMobileFileIntegrityUserClient17validateSignatureEP8OSObjectPvP25IOExternalMethodArgumentsDCQ __ZN34AppleMobileFileIntegrityUserClient11setDenylistEP8OSObjectPvP25IOExternalMethodArguments
3.2.3.1 loadTrustCache
_int64 __fastcall AppleMobileFileIntegrityUserClient::loadTrustCache(AppleMobileFileIntegrityUserClient *this,OSObject *a2,void *a3,IOExternalMethodArguments *a4){v6 = proc_selfpid();IOLog("%s: PID %d is requesting a trust cache load\n","static IOReturn AppleMobileFileIntegrityUserClient::loadTrustCache(OSObject *, void *, IOExternalMethodArguments *)",v6);
每次加载都产生一次日志。
Bool = AppleMobileFileIntegrity::AMFIEntitlementGetBool(v9,(proc *)"com.apple.private.amfi.can-load-trust-cache",&v30,v10);proc_rele(v11);if ( Bool ){proc_selfpid();IOLog("%s: failure getting load trust cache entitlement for process %d\n");return 3758097089LL;}if ( !v30 ){proc_selfpid();IOLog("%s: process %d not allowed to load trust cache\n");return 3758097089LL;}
判断当前进程是否由com.apple.private.amfi.can-load-trust-cache这个entitlement。
v17 = *((unsigned int *)a3 + 1);if ( (_DWORD)v17 == 7 ){
V17代表trustcache类型,为7表示携带manifest信息。
v19 = AppleMobileFileIntegrityUserClient::copyTrustCacheAndManifestFromInputArgument((AppleMobileFileIntegrityUserClient *)a3,(IOExternalMethodArguments *)&v28,(void **)&v29,(unsigned __int64 *)&v26,(void **)&v27,&v24,&v25,v14);v15 = v19;TrustCacheWithExternalManifest = AppleMobileFileIntegrity::loadTrustCacheWithExternalManifest(v8,v26,v27,(void *)v24,(unsigned __int64)v25);
copyTrustCacheAndManifestFromInputArgument函数做格式检查,最后调用loadTrustCacheWithExternalManifest函数装载trustcache。
如果v17不为7,代表trustcache不携带manifest信息,调用loadTrustCache函数进行装载。
loadTrustCacheWithExternalManifest和loadTrustCache最终会调用loadTrustCacheWithExternalManifest函数,它又进一步调用pmap_load_image4_trust_cache,这个函数是ppl提供的服务函数。
它的功能是将一个imag4类型的trustcache保存到ppl中。
第一个参数指向struct image4_trust_cache结构体,第二个参数为它的大小,第三个参数为image4 manifest buffer,结构不详,第四个参数为manifest buffer大小。
__PPLTEXT:__text:FFFFFFF0097FE078 BL _pmap_reserve_ppl_page__PPLTEXT:__text:FFFFFFF0097FE07C CMP W0, #6__PPLTEXT:__text:FFFFFFF0097FE080 B.NE loc_FFFFFFF0097FE08C首先调用_pmap_reserve_ppl_page从ppl中获取一个page,然后挂接到_pmap_reserved_page_list中。__PPLTEXT:__text:FFFFFFF0097FE08C loc_FFFFFFF0097FE08C ; CODE XREF: _pmap_load_image4_trust_cache_internal+48↑j__PPLTEXT:__text:FFFFFFF0097FE08C MOV W8, #0x10000000__PPLTEXT:__text:FFFFFFF0097FE090 CMP X21, X8__PPLTEXT:__text:FFFFFFF0097FE094 B.LS loc_FFFFFFF0097FE0A0 ; x1 < 0x10000000__PPLTEXT:__text:FFFFFFF0097FE098 MOV W25, #0xFFFFFFFA__PPLTEXT:__text:FFFFFFF0097FE09C B loc_FFFFFFF0097FE30C__PPLTEXT:__text:FFFFFFF0097FE0A0 ; ---------------------------------------------------------------------------__PPLTEXT:__text:FFFFFFF0097FE0A0__PPLTEXT:__text:FFFFFFF0097FE0A0 loc_FFFFFFF0097FE0A0 ; CODE XREF: _pmap_load_image4_trust_cache_internal+5C↑j__PPLTEXT:__text:FFFFFFF0097FE0A0 MOV W8, #0x10000000__PPLTEXT:__text:FFFFFFF0097FE0A4 CMP X19, X8__PPLTEXT:__text:FFFFFFF0097FE0A8 B.LS loc_FFFFFFF0097FE0B4 ; x3 < 0x10000000__PPLTEXT:__text:FFFFFFF0097FE0AC MOV W25, #0xFFFFFFF8__PPLTEXT:__text:FFFFFFF0097FE0B0 B loc_FFFFFFF0097FE30C__PPLTEXT:__text:FFFFFFF0097FE0B4 ; -----------------------------------------------------__PPLTEXT:__text:FFFFFFF0097FE0B4 loc_FFFFFFF0097FE0B4 ; CODE XREF: _pmap_load_image4_trust_cache_internal+70↑j__PPLTEXT:__text:FFFFFFF0097FE0B4 CMN X22, X21__PPLTEXT:__text:FFFFFFF0097FE0B8 B.CS loc_FFFFFFF0097FE514 ; x0 + x1 < x0__PPLTEXT:__text:FFFFFFF0097FE0BC CMN X20, X19__PPLTEXT:__text:FFFFFFF0097FE0C0 B.CS loc_FFFFFFF0097FE524 ; x3 + x4 < x3__PPLTEXT:__text:FFFFFFF0097FE0C4 TST X22, #0x3FFF__PPLTEXT:__text:FFFFFFF0097FE0C8 B.NE loc_FFFFFFF0097FE534 ; x0 & 0x3fff__PPLTEXT:__text:FFFFFFF0097FE0CC TST X21, #0x3FFF ; x1 & 0x3fff__PPLTEXT:__text:FFFFFFF0097FE0D0 B.NE loc_FFFFFFF0097FE544__PPLTEXT:__text:FFFFFFF0097FE0D4 TST X20, #0x3FFF__PPLTEXT:__text:FFFFFFF0097FE0D8 B.NE loc_FFFFFFF0097FE554 ; x2 & 0x3fff__PPLTEXT:__text:FFFFFFF0097FE0DC TST X19, #0x3FFF__PPLTEXT:__text:FFFFFFF0097FE0E0 B.NE loc_FFFFFFF0097FE564 ; x3 & 0x3fff__PPLTEXT:__text:FFFFFFF0097FE0E4 CMP X21, #0x57 ; 'W'__PPLTEXT:__text:FFFFFFF0097FE0E8 B.LS loc_FFFFFFF0097FE574 ; x1 < 0x57
一个image4_trust_cache和其manifest buffer地址必须以0x4000对齐,并且大小不能超过0x10000000, image4_trust_cache不能小于0x57。
__PPLTEXT:__text:FFFFFFF0097FE0EC MOV X0, X22__PPLTEXT:__text:FFFFFFF0097FE0F0 MOV X1, X21__PPLTEXT:__text:FFFFFFF0097FE0F4 BL _pmap_ppl_lockdown_pages__PPLTEXT:__text:FFFFFFF0097FE0F8 CBZ X19, loc_FFFFFFF0097FE108__PPLTEXT:__text:FFFFFFF0097FE0FC MOV X0, X20__PPLTEXT:__text:FFFFFFF0097FE100 MOV X1, X19__PPLTEXT:__text:FFFFFFF0097FE104 BL _pmap_ppl_lockdown_pages
调用_pmap_ppl_lockdown_pages在ppl中分别锁定image4_trust_cache和manifest buffer物理页。
__PPLTEXT:__text:FFFFFFF0097FE128 BL _pmap_get_current_csidentity获取调用进程的csidentity。
__PPLTEXT:__text:FFFFFFF0097FE130 ADRL X8, aComAppleMobile ; "com.apple.mobile.softwareupdated"__PPLTEXT:__text:FFFFFFF0097FE138 MOV X9, X0__PPLTEXT:__text:FFFFFFF0097FE13C__PPLTEXT:__text:FFFFFFF0097FE13C loc_FFFFFFF0097FE13C ; CODE XREF: _pmap_load_image4_trust_cache_internal+11C↓j__PPLTEXT:__text:FFFFFFF0097FE13C LDRB W10, [X9]__PPLTEXT:__text:FFFFFFF0097FE140 LDRB W11, [X8] ; "com.apple.mobile.softwareupdated"__PPLTEXT:__text:FFFFFFF0097FE144 CMP W10, W11__PPLTEXT:__text:FFFFFFF0097FE148 B.NE loc_FFFFFFF0097FE16C__PPLTEXT:__text:FFFFFFF0097FE14C ADD X8, X8, #1__PPLTEXT:__text:FFFFFFF0097FE150 ADD X9, X9, #1__PPLTEXT:__text:FFFFFFF0097FE154 CBNZ W10, loc_FFFFFFF0097FE13C__PPLTEXT:__text:FFFFFFF0097FE158
通过上述代码判断进程的csidentity是否为com.apple.mobile.softwareupdated、com.apple.load_trust_cache、com.apple.security.cryptexd、com.apple.MobileStorageMounter、
com.apple.security.cryptexctl。如果进程csidentity不是这些中的一个,则直接返回。
__PPLTEXT:__text:FFFFFFF0097FE22C loc_FFFFFFF0097FE22C ; CODE XREF: _pmap_load_image4_trust_cache_internal+128↑j__PPLTEXT:__text:FFFFFFF0097FE22C ; _pmap_load_image4_trust_cache_internal+164↑j ...__PPLTEXT:__text:FFFFFFF0097FE22C ADD X27, X22, #0x58 ; 'X'__PPLTEXT:__text:FFFFFFF0097FE230 LDR X1, [X22,#0x50]__PPLTEXT:__text:FFFFFFF0097FE234 ADD X6, SP, #0x70+var_58 ; uuid__PPLTEXT:__text:FFFFFFF0097FE238 ADD X7, SP, #0x70+var_60__PPLTEXT:__text:FFFFFFF0097FE23C MOV X0, X27__PPLTEXT:__text:FFFFFFF0097FE240 MOV X2, X20__PPLTEXT:__text:FFFFFFF0097FE244 MOV X3, X24__PPLTEXT:__text:FFFFFFF0097FE248 MOV W4, #0x6C747273__PPLTEXT:__text:FFFFFFF0097FE250 MOV X5, X26__PPLTEXT:__text:FFFFFFF0097FE254 BL _pmap_validate_image4__PPLTEXT:__text:FFFFFFF0097FE258 CBZ W0, loc_FFFFFFF0097FE2A4__PPLTEXT:__text:FFFFFFF0097FE25C MOV X25, X0__PPLTEXT:__text:FFFFFFF0097FE260 CBZ W28, loc_FFFFFFF0097FE2EC__PPLTEXT:__text:FFFFFFF0097FE264 CMP W26, #1__PPLTEXT:__text:FFFFFFF0097FE268 B.NE loc_FFFFFFF0097FE2EC__PPLTEXT:__text:FFFFFFF0097FE26C CMP W25, W28__PPLTEXT:__text:FFFFFFF0097FE270 B.NE loc_FFFFFFF0097FE2EC__PPLTEXT:__text:FFFFFFF0097FE274 LDR X1, [X22,#0x50]__PPLTEXT:__text:FFFFFFF0097FE278 ADD X6, SP, #0x70+var_58__PPLTEXT:__text:FFFFFFF0097FE27C ADD X7, SP, #0x70+var_60__PPLTEXT:__text:FFFFFFF0097FE280 MOV X0, X27__PPLTEXT:__text:FFFFFFF0097FE284 MOV X2, X20__PPLTEXT:__text:FFFFFFF0097FE288 MOV X3, X24__PPLTEXT:__text:FFFFFFF0097FE28C MOV W4, #0x6C747273__PPLTEXT:__text:FFFFFFF0097FE294 MOV W5, #4__PPLTEXT:__text:FFFFFFF0097FE298 BL _pmap_validate_image4__PPLTEXT:__text:FFFFFFF0097FE29C MOV X25, X0__PPLTEXT:__text:FFFFFFF0097FE2A0 CBNZ W0, loc_FFFFFFF0097FE2EC
调用_pmap_validate_image4验证image4_trust_cache和其manifest buffer结构合法性,var_58保存的是image4的uuid。
__PPLTEXT:__text:FFFFFFF0097FE330 loc_FFFFFFF0097FE330 ; CODE XREF: _pmap_load_image4_trust_cache_internal+2A4↑j__PPLTEXT:__text:FFFFFFF0097FE330 ADR X24, _pmap_loaded_trust_caches_lock__PPLTEXT:__text:FFFFFFF0097FE334 NOP__PPLTEXT:__text:FFFFFFF0097FE338 MOV X0, X24__PPLTEXT:__text:FFFFFFF0097FE33C BL _hw_lock_lock_nopreempt__PPLTEXT:__text:FFFFFFF0097FE340 ADRP X27, #_pmap_image4_trust_caches@PAGE__PPLTEXT:__text:FFFFFFF0097FE344 LDR X8, [X27,#_pmap_image4_trust_caches@PAGEOFF]__PPLTEXT:__text:FFFFFFF0097FE348 CBZ X8, loc_FFFFFFF0097FE478__PPLTEXT:__text:FFFFFFF0097FE34C LDR X9, [SP,#0x70+var_58]__PPLTEXT:__text:FFFFFFF0097FE350 B loc_FFFFFFF0097FE35C ; struct trust_cache_entry1 {
pmap_image4_trust_caches数组保存的是ppl中所有的image4结构体内容,xnu源码在某个版本中泄露过struct image4_trust_cache的结构体。
osfmk/kern/trustcache.hstruct trust_cache_entry1 {uint8_t cdhash[CS_CDHASH_LEN];uint8_t hash_type;uint8_t flags;} __attribute__((__packed__));struct trust_cache_module1 {uint32_t version;uuid_t uuid;uint32_t num_entries;struct trust_cache_entry1 entries[];} __attribute__((__packed__));osfmk/vm/pmap.hstruct pmap_image4_trust_cache {// Filled by pmap layer.struct pmap_image4_trust_cache const *next; // linked list linkagestruct trust_cache_module1 const *module; // pointer into module (within data below)// Filled by caller.// data is either an image4,// or just the trust cache payload itself if the image4 manifest is external.pmap_tc_type_t type;size_t bnch_len;uint8_t const bnch[48];size_t data_len;uint8_t const data[];};
__PPLTEXT:__text:FFFFFFF0097FE354 loc_FFFFFFF0097FE354 ; CODE XREF: _pmap_load_image4_trust_cache_internal+334↓j__PPLTEXT:__text:FFFFFFF0097FE354 ; _pmap_load_image4_trust_cache_internal+344↓j ...__PPLTEXT:__text:FFFFFFF0097FE354 LDR X8, [X8] ; cache_entry = cache_entry->next__PPLTEXT:__text:FFFFFFF0097FE358 CBZ X8, loc_FFFFFFF0097FE478__PPLTEXT:__text:FFFFFFF0097FE35C__PPLTEXT:__text:FFFFFFF0097FE35C loc_FFFFFFF0097FE35C ; CODE XREF: _pmap_load_image4_trust_cache_internal+318↑j__PPLTEXT:__text:FFFFFFF0097FE35C LDR X10, [X8,#8] ; module = cache_entry->module__PPLTEXT:__text:FFFFFFF0097FE360 LDRB W11, [X9,#4]__PPLTEXT:__text:FFFFFFF0097FE364 LDRB W12, [X10,#4] ; module->uuid[0]__PPLTEXT:__text:FFFFFFF0097FE368 CMP W11, W12__PPLTEXT:__text:FFFFFFF0097FE36C B.NE loc_FFFFFFF0097FE354 ; cache_entry = cache_entry->next__PPLTEXT:__text:FFFFFFF0097FE370 LDRB W11, [X9,#5]__PPLTEXT:__text:FFFFFFF0097FE374 LDRB W12, [X10,#5] ; module->uuid[1]__PPLTEXT:__text:FFFFFFF0097FE378 CMP W11, W12__PPLTEXT:__text:FFFFFFF0097FE37C B.NE loc_FFFFFFF0097FE354 ; cache_entry = cache_entry->next__PPLTEXT:__text:FFFFFFF0097FE380 LDRB W11, [X9,#6]__PPLTEXT:__text:FFFFFFF0097FE384 LDRB W12, [X10,#6] ; module->uuid[2]__PPLTEXT:__text:FFFFFFF0097FE388 CMP W11, W12__PPLTEXT:__text:FFFFFFF0097FE38C B.NE loc_FFFFFFF0097FE354 ; cache_entry = cache_entry->next__PPLTEXT:__text:FFFFFFF0097FE390 LDRB W11, [X9,#7]__PPLTEXT:__text:FFFFFFF0097FE394 LDRB W12, [X10,#7] ; module->uuid[3]__PPLTEXT:__text:FFFFFFF0097FE398 CMP W11, W12__PPLTEXT:__text:FFFFFFF0097FE39C B.NE loc_FFFFFFF0097FE354 ; cache_entry = cache_entry->next__PPLTEXT:__text:FFFFFFF0097FE3A0 LDRB W11, [X9,#8]__PPLTEXT:__text:FFFFFFF0097FE3A4 LDRB W12, [X10,#8] ; module->uuid[4]__PPLTEXT:__text:FFFFFFF0097FE3A8 CMP W11, W12__PPLTEXT:__text:FFFFFFF0097FE3AC B.NE loc_FFFFFFF0097FE354 ; cache_entry = cache_entry->next__PPLTEXT:__text:FFFFFFF0097FE3B0 LDRB W11, [X9,#9]__PPLTEXT:__text:FFFFFFF0097FE3B4 LDRB W12, [X10,#9] ; module->uuid[5]__PPLTEXT:__text:FFFFFFF0097FE3B8 CMP W11, W12__PPLTEXT:__text:FFFFFFF0097FE3BC B.NE loc_FFFFFFF0097FE354 ; cache_entry = cache_entry->next__PPLTEXT:__text:FFFFFFF0097FE3C0 LDRB W11, [X9,#0xA]__PPLTEXT:__text:FFFFFFF0097FE3C4 LDRB W12, [X10,#0xA] ; module->uuid[6]__PPLTEXT:__text:FFFFFFF0097FE3C8 CMP W11, W12__PPLTEXT:__text:FFFFFFF0097FE3CC B.NE loc_FFFFFFF0097FE354 ; cache_entry = cache_entry->next__PPLTEXT:__text:FFFFFFF0097FE3D0 LDRB W11, [X9,#0xB]__PPLTEXT:__text:FFFFFFF0097FE3D4 LDRB W12, [X10,#0xB] ; module->uuid[7]__PPLTEXT:__text:FFFFFFF0097FE3D8 CMP W11, W12__PPLTEXT:__text:FFFFFFF0097FE3DC B.NE loc_FFFFFFF0097FE354 ; cache_entry = cache_entry->next__PPLTEXT:__text:FFFFFFF0097FE3E0 LDRB W11, [X9,#0xC]__PPLTEXT:__text:FFFFFFF0097FE3E4 LDRB W12, [X10,#0xC] ; module->uuid[8]__PPLTEXT:__text:FFFFFFF0097FE3E8 CMP W11, W12__PPLTEXT:__text:FFFFFFF0097FE3EC B.NE loc_FFFFFFF0097FE354 ; cache_entry = cache_entry->next__PPLTEXT:__text:FFFFFFF0097FE3F0 LDRB W11, [X9,#0xD]__PPLTEXT:__text:FFFFFFF0097FE3F4 LDRB W12, [X10,#0xD] ; module->uuid[9]__PPLTEXT:__text:FFFFFFF0097FE3F8 CMP W11, W12__PPLTEXT:__text:FFFFFFF0097FE3FC B.NE loc_FFFFFFF0097FE354 ; cache_entry = cache_entry->next__PPLTEXT:__text:FFFFFFF0097FE400 LDRB W11, [X9,#0xE]__PPLTEXT:__text:FFFFFFF0097FE404 LDRB W12, [X10,#0xE] ; module->uuid[10]__PPLTEXT:__text:FFFFFFF0097FE408 CMP W11, W12__PPLTEXT:__text:FFFFFFF0097FE40C B.NE loc_FFFFFFF0097FE354 ; cache_entry = cache_entry->next__PPLTEXT:__text:FFFFFFF0097FE410 LDRB W11, [X9,#0xF]__PPLTEXT:__text:FFFFFFF0097FE414 LDRB W12, [X10,#0xF] ; module->uuid[11]__PPLTEXT:__text:FFFFFFF0097FE418 CMP W11, W12__PPLTEXT:__text:FFFFFFF0097FE41C B.NE loc_FFFFFFF0097FE354 ; cache_entry = cache_entry->next__PPLTEXT:__text:FFFFFFF0097FE420 LDRB W11, [X9,#0x10]__PPLTEXT:__text:FFFFFFF0097FE424 LDRB W12, [X10,#0x10] ; module->uuid[12]__PPLTEXT:__text:FFFFFFF0097FE428 CMP W11, W12__PPLTEXT:__text:FFFFFFF0097FE42C B.NE loc_FFFFFFF0097FE354 ; cache_entry = cache_entry->next__PPLTEXT:__text:FFFFFFF0097FE430 LDRB W11, [X9,#0x11]__PPLTEXT:__text:FFFFFFF0097FE434 LDRB W12, [X10,#0x11] ; module->uuid[13]__PPLTEXT:__text:FFFFFFF0097FE438 CMP W11, W12__PPLTEXT:__text:FFFFFFF0097FE43C B.NE loc_FFFFFFF0097FE354 ; cache_entry = cache_entry->next__PPLTEXT:__text:FFFFFFF0097FE440 LDRB W11, [X9,#0x12]__PPLTEXT:__text:FFFFFFF0097FE444 LDRB W12, [X10,#0x12] ; module->uuid[14]__PPLTEXT:__text:FFFFFFF0097FE448 CMP W11, W12__PPLTEXT:__text:FFFFFFF0097FE44C B.NE loc_FFFFFFF0097FE354 ; cache_entry = cache_entry->next__PPLTEXT:__text:FFFFFFF0097FE450 LDRB W11, [X9,#0x13]__PPLTEXT:__text:FFFFFFF0097FE454 LDRB W10, [X10,#0x13] ; module->uuid[15]__PPLTEXT:__text:FFFFFFF0097FE458 CMP W11, W10__PPLTEXT:__text:FFFFFFF0097FE45C B.NE loc_FFFFFFF0097FE354 ; cache_entry = cache_entry->next
通过一个循环判断要保存的image4 cache对应的uuid是否已经在pmap_image4_trust_caches数组中。
__PPLTEXT:__text:FFFFFFF0097FE478 loc_FFFFFFF0097FE478 ; CODE XREF: _pmap_load_image4_trust_cache_internal+310↑j__PPLTEXT:__text:FFFFFFF0097FE478 ; _pmap_load_image4_trust_cache_internal+320↑j__PPLTEXT:__text:FFFFFFF0097FE478 MOV X0, X22__PPLTEXT:__text:FFFFFFF0097FE47C BL _mmu_kvtop__PPLTEXT:__text:FFFFFFF0097FE480 CBNZ X0, loc_FFFFFFF0097FE498__PPLTEXT:__text:FFFFFFF0097FE484 ADRP X8, #_kernel_pmap@PAGE__PPLTEXT:__text:FFFFFFF0097FE488 LDR X0, [X8,#_kernel_pmap@PAGEOFF]__PPLTEXT:__text:FFFFFFF0097FE48C MOV X1, X22__PPLTEXT:__text:FFFFFFF0097FE490 BL _pmap_vtophys__PPLTEXT:__text:FFFFFFF0097FE494 LSL X0, X0, #0xE__PPLTEXT:__text:FFFFFFF0097FE498__PPLTEXT:__text:FFFFFFF0097FE498 loc_FFFFFFF0097FE498 ; CODE XREF: _pmap_load_image4_trust_cache_internal+448↑j__PPLTEXT:__text:FFFFFFF0097FE498 BL _phystokv__PPLTEXT:__text:FFFFFFF0097FE49C MOV X25, X0 ; start__PPLTEXT:__text:FFFFFFF0097FE4A0 ADD X26, X0, #4,LSL#12 ; end = x0 + 4 << 12__PPLTEXT:__text:FFFFFFF0097FE4A4 MOV X1, X26__PPLTEXT:__text:FFFFFFF0097FE4A8 MOV W2, #0xB__PPLTEXT:__text:FFFFFFF0097FE4AC MOV W3, #1__PPLTEXT:__text:FFFFFFF0097FE4B0 BL _pmap_set_range_xprr_perm ; (start, end, 0xb, 1)__PPLTEXT:__text:FFFFFFF0097FE4B4 LDR X8, [SP,#0x70+var_58]__PPLTEXT:__text:FFFFFFF0097FE4B8 STR X8, [X25,#8] ; *(start + 8) = var_58__PPLTEXT:__text:FFFFFFF0097FE4BC LDR X8, [X27,#_pmap_image4_trust_caches@PAGEOFF]__PPLTEXT:__text:FFFFFFF0097FE4C0 STR X8, [X25] ; *(start + 0) = _pmap_image4_trust_caches__PPLTEXT:__text:FFFFFFF0097FE4C4 MOV X0, X25__PPLTEXT:__text:FFFFFFF0097FE4C8 MOV X1, X26__PPLTEXT:__text:FFFFFFF0097FE4CC MOV W2, #1__PPLTEXT:__text:FFFFFFF0097FE4D0 MOV W3, #0xB__PPLTEXT:__text:FFFFFFF0097FE4D4 BL _pmap_set_range_xprr_perm ; (start, end, 1, 0xb)__PPLTEXT:__text:FFFFFFF0097FE4F4 loc_FFFFFFF0097FE4F4 ; CODE XREF: _pmap_load_image4_trust_cache_internal+4A0↑j__PPLTEXT:__text:FFFFFFF0097FE4F4 STR X22, [X27,#_pmap_image4_trust_caches@PAGEOFF] ; _pmap_image4_trust_caches = x0
调用pmap_set_range_xprr_perm设置image4 cache的权限, Image4 cache的next字段设置为pmap_image4_trust_caches,
然后将image4 cache挂接为pmap_image4_trust_caches数组的第一个元素。
3.2.3.2 flushAllValidations
__int64 __fastcall AppleMobileFileIntegrityUserClient::flushAllValidations(AppleMobileFileIntegrityUserClient *this,OSObject *a2,void *a3,IOExternalMethodArguments *a4){if ( v4 && (v5 = (AppleMobileFileIntegrity *)v4[27].__vftable) != 0LL )return AppleMobileFileIntegrity::purgeCachedValidationResults(v5);elsereturn 3758097097LL;}__int64 __fastcall AppleMobileFileIntegrity::purgeCachedValidationResults(AppleMobileFileIntegrity *this){cs_blob_reset_cache(this);denyUnrestrictedDebuggingCache = 0;return 0LL;}void cs_blob_reset_cache(){atomic_fetch_add_explicit(&cs_blob_generation_count, 2u, memory_order_relaxed);}
3.2.3.3 loadCompilationServiceCodeDirectoryHash
__int64 __fastcall AppleMobileFileIntegrityUserClient::loadCompilationServiceCodeDirectoryHash(AppleMobileFileIntegrityUserClient *this,OSObject *a2,void *a3,IOExternalMethodArguments *a4){Bool = AppleMobileFileIntegrity::AMFIEntitlementGetBool(v10,(proc *)"com.apple.private.amfi.can-load-cdhash",&v15,v11);proc_rele(v12);
判断当前进程是否有com.apple.private.amfi.can-load-cdhash这个entitlement。
return AppleMobileFileIntegrity::loadCompilationServiceCodeDirectoryHash(v6, v16);调用loadCompilationServiceCodeDirectoryHash装载compilationServiceHash。
__int64 __fastcall AppleMobileFileIntegrity::loadCompilationServiceCodeDirectoryHash(AppleMobileFileIntegrity *this,const unsigned __int8 *a2){__int128 v3; // q0lck_mtx_lock((IOLock *)compServiceHashLock);v3 = *(_OWORD *)a2;dword_FFFFFFF009A584B0 = *((_DWORD *)a2 + 4);compilationServiceHash = v3;lck_mtx_unlock((IOLock *)compServiceHashLock);return 0LL;}
3.2.3.4 isCdhashInTrustCache
__int64 __fastcall AppleMobileFileIntegrityUserClient::isCdhashInTrustCache(AppleMobileFileIntegrityUserClient *this,OSObject *a2,void *a3,IOExternalMethodArguments *a4){Bool = AppleMobileFileIntegrity::isCdhashInTrustCache(v6,*((unsigned __int8 **)a3 + 6),*((_QWORD *)a3 + 4) != 0LL);proc_selfpid();IOLog("%s: Returning IOReturn 0x%x to process %d\n");
调用isCdhashInTrustCache检查用户态进程传递进来的cd hash是否在trustcache中。
__int64 __fastcall amfi_is_cdhash_in_trust_cache(constunsigned __int8 *a1, int a2){__int64 result; // x0result = AMFIIsCodeDirectoryInTrustCache(a1);if ( (result & 1) == 0 ){if ( a2 )return (unsigned int)codeDirectoryHashIsInLoadedTrustCache(a1) != 0;}return result;}
AMFIIsCodeDirectoryInTrustCache和codeDirectoryHashIsInLoadedTrustCache调用ppl服务函数,检查cd hash是否在static trustcache和loaded trustcache中。
3.2.3.5 validateSignature
AMFI允许用户进程主动传递一个cs_blob来做验签,最终都会调用check_signature函数,参见第2节。
3.2.3.6 setDenylist
__int64 __fastcall AppleMobileFileIntegrityUserClient::setDenylist(AppleMobileFileIntegrityUserClient *this,OSObject *a2,void *a3,IOExternalMethodArguments *a4){Bool = AppleMobileFileIntegrity::AMFIEntitlementGetBool(v6,(proc *)"com.apple.private.amfi.can-set-denylist",&v18,v7);
判断当前进程是否有com.apple.private.amfi.can-set-denylist这个entitlement。
if ( (ValidateBlacklist(v15, v11) & 1) != 0 ){UpdateBlacklist(v15, v11);return 0LL;}
ValidateBlacklist函数判断要检查的cd hash是否已在blacklist中。
void __fastcall UpdateBlacklist(__int64 a1, __int64 a2){lck_rw_lock_exclusive((IORWLock *)blacklistLock);IOFree((void *)blacklist, 32 * blacklistSize);blacklist = a1;blacklistSize = a2;lck_rw_done((IORWLock *)blacklistLock);}
释放原有blacklist内存,更新blacklist新内存。
3.2.4 用户态查询与设置接口
内核提供了csops_internal函数用于接收用户态参数,对以下进程的签名信息进行设置与读取。
Bsd/sys/codesign.h/* csopsoperations *//* CS_OPS_PIDPATH 4 */
3.3 PPL保护
3.3.1 pmap_cs_cd_register_internal
ios将code directory存放到ppl进行保护。ppl中的服务函数为pmap_cs_cd_register_internal,它的功能是将每个进程的csblob结构体放到ppl中进行保护,同时将其部分内容算sha256哈希,存放于一个特殊的cd结构体中,这个结构体又被红黑树管理起来。
第一个参数为struct cs_blob结构体,第二个参数为struct cs_blob结构体的长度,第三个参数为struct cs_blob结构里的某个offset,这个函数将csblob结构体保存在ppl中,
同时计算struct cs_blob从offset开始的hash并存放于ppl中,第四、五个参数功能不详。
由于这个函数非常复杂,只贴出部分关键操作的代码。
MOV X9, #0xFFFFFFE000000000CMP X0, X9B.CC loc_FFFFFFF0097FDA8C ; x0 < 0xFFFFFFE000000000TBNZ W8, #0, loc_FFFFFFF0097FDA8C ; x0 + x1 < x0ADD X9, X20, X21MOV X8, #0xFFFFFFFC00000000CMP X9, X8B.CS loc_FFFFFFF0097FDA8C ; x0 + x1 > 0xFFFFFFFC00000000MOV X24, X2ADDS X8, X2, X21CSET W10, CSSUBS X26, X20, X2 ; offset = x1 - x2B.CC loc_FFFFFFF0097FDA9C ; offset < 0TBNZ W10, #0, loc_FFFFFFF0097FDA9C ; x0 + x2 < x0CMP X8, X9B.HI loc_FFFFFFF0097FDA9C ; x1 + x2 > x1 + x0
上述代码对前三个参数做了范围检查。
MOV W8, #0x1FF8CMP X20, X8STR X4,B.HI loc_FFFFFFF0097FD3AC ; x1 > 0x1ff8 x23 = 0CMP X20, #0x1F9B.CC loc_FFFFFFF0097FD3B4 ; x1 < 0x1f9 x8 = 0CMP X20, #0x3F9 ; x1 < 0x3f9 x8 = 1B.CC loc_FFFFFFF0097FD3BCCMP X20, #0x7F9B.CC loc_FFFFFFF0097FD3C4 ; x1 < 0x7f9 x8 = 2CMP X20, #0xFF8MOV W8, #3CINC W8, W8, HIB loc_FFFFFFF0097FD3C8 ; if x1 < 0xff8x8 = 3elsex8 = 4MOV X23, #0B loc_FFFFFFF0097FD4B8MOV W8, #0B loc_FFFFFFF0097FD3C8MOV W8, #1B loc_FFFFFFF0097FD3C8MOV W8, #2
根据第2个参数计算出一个index,它是struct csblob _pmap_cs_blob_free_list[]数组的一个索引。在xnu的源码bsd/sys/ubc_internal.h中可以看到struct csblob根据config的不同,
有不同的大小。
MOV W22, W8LSL X27, X22, #3 ; index = w8 * 8ADR X25, _pmap_cs_blob_free_list_lockNOPMOV X0, X25BL _hw_lock_lock_nopreemptADR X8, _pmap_cs_blob_free_list ; uint64_t _pmap_cs_blob_free_list[]NOPLDR X23, [X8,X27] ; csblob_struct =_pmap_cs_blob_free_list[index]CBZ X23, loc_FFFFFFF0097FD428LDR X9, [X23] ; next = csblob_struct->nextSTR X9, [X8,X22,LSL#3] ; _pmap_cs_blob_free_list[index]=nextSTR X19, [X23] ; csblob_struct->next=0xDEADEAD0A110CEDLSL X8, X22, #2 ; index1=w8 * 4ADR X9, _pmap_cs_blob_free_countNOPLDR W10, [X9,X8]SUB W10, W10, #1STR W10, [X9,X8] ; _pmap_cs_blob_free_count[index1] -= 1
根据index,从_pmap_cs_blob_free_list找到一个空闲的块, pmap_cs_blob_free_count数组保存的是对应的cs_blob大小的空闲数目。
如果_pmap_cs_blob_free_list对应的节点为空,则要申请一个新的page。
MRS X8, #0, c13, c0, #4LDR W8,CBZ W8, loc_FFFFFFF0097FDB14STLR XZR,BL _pmap_get_free_ppl_pageCBZ X0, loc_FFFFFFF0097FD900MOV X23, X0 ; new_blob_pageADRL X8, _pmap_cs_blob_binsLDR X25,BL _phystokvMOV W27, #0x4000MOV W1, #0x4000 ; size_tBL _bzero ; bzero(new_blog_page, 0x4000)MOV X0, X23BL _phystokvMOV X23, X0MOV X1, #0MOV X11, #0STR X19,AND X8, X25, #0xFFFFFFFFFFFFFFF8 ; bin_size &= 0xFFFFFFFFFFFFFFF8MOV W9, #2 ; x9 = 2UDIV X10, X27, X25 ; num = 0x4000 / bin_size
_pmap_get_free_ppl_page从ppl中申请了一个新的物理页,通过_phystokv转化为虚拟地址,然后用bzero进行初始化。前面将csblob有不同的大小,对一个新分配好的page要对它进行切块分配,自然要知道每个块的大小,_pmap_cs_blob_bins数组保存的就是对应的csblob大小。最后x10保存的是一个page对应的csblob块大小,然后通过一个循环对其进行初始化。
ADD X0, X0, X8 ; current_page = new_blob_page; next_page = 0; do {; current_page += bin_sizeSTR X11, [X0] ; *(current_page->next) = next_pageCMP X1, #0CSEL X1, X0, X1, EQ ; if (x1 == 0)x1 = x0ADD W12, W9, #1CMP X10, W9,UXTWMOV X9, X12 ; x9 += 1MOV X11, X0 ; next_page = current_pageB.HI loc_FFFFFFF0097FD484 ; while (num > x9)SUB W3, W12, #2 ; num -= 2MOV X2, X22 ; indexMOV W4, #1BL _pmap_cs_blob_add_to_free_list
这个循环是对每个csblob的第一个成员,也就是next指针进行初始化,注意每个page的第一个csblob是保留不用的,没有被链接起来,它的next是指向0xDEADEAD0A110CED。
最后通过_pmap_cs_blob_add_to_free_list把这些块加入到现有链表中。
__int64 __fastcall pmap_cs_blob_add_to_free_list(void *a1, _QWORD *a2, __int64 a3, __int64 a4, __int64 a5){v7 = pmap_cs_blob_bins[(unsigned int)a3];if ( ((unsigned __int16)a1 & 0x3FFFu) % v7 || ((unsigned __int16)a2 & 0x3FFFu) % v7 )panic("\"free blob to add unaligned: (%p,%p)/%zu\"", a1, a2, v7);v11 = (unsigned int)a3;result = hw_lock_lock_nopreempt(&pmap_cs_blob_free_list_lock);*a2 = *(&pmap_cs_blob_free_list + v11);*(&pmap_cs_blob_free_list + v11) = a1;v13 = pmap_cs_blob_free_count[v11];pmap_cs_blob_free_count[v11] = v13 + a4;if ( __CFADD__(v13, (_DWORD)a4) )panic("\"blob free count overflow (bin %d, %d/%d)\"", a3, (unsigned int)pmap_cs_blob_free_count[v11], a4);v14 = pmap_cs_blob_page_count[v11];pmap_cs_blob_page_count[v11] = v14 + a5;}
上述代码将链表头链接到新page的最有一个块,然后更新对应的free节点数目。
MRS X8, #0, c13, c0, #4LDR W8,CBZ W8, loc_FFFFFFF0097FDAC4STLR XZR,MOV W8, #0x1FF9CMP X20, X8B.CS loc_FFFFFFF0097FD5E4 ; x1 > 0x1ff9ADD X25, X23, #8MOV X0, X25 ; __dstMOV X1, X21 ; __srcMOV X2, X20 ; __nBL _memmove ; memmove(new_blog_page + 8, x0, x1)
使用memove将第一个参数指向的csblob结构体拷贝到刚申请到内存块中。
ADR X25, _pmap_cs_cd_free_list_lockNOPMOV X0, X25BL _hw_lock_lock_nopreemptADRP X22, #_pmap_cs_cd_free_list@PAGELDR X28, [X22,#_pmap_cs_cd_free_list@PAGEOFF]ADRP X27, #_pmap_cs_cd_free_count@PAGECBZ X28, loc_FFFFFFF0097FD4F4 ; _pmap_cs_cd_free_list == 0LDR X8, [X28] ; next = *pmap_cs_cd_free_listSTR X8, [X22,#_pmap_cs_cd_free_list@PAGEOFF] ; pmap_cs_cd_free_list = nextSTR X19, [X28] ; *next = 0xDEADEAD0A110CEDLDR W8, [X27,#_pmap_cs_cd_free_count@PAGEOFF]SUB W8, W8, #1STR W8, [X27,#_pmap_cs_cd_free_count@PAGEOFF] ; _pmap_cs_cd_free_count[index] -= 1
在获取一个csblob结构体后,接着要从_pmap_cs_cd_free_list链表分配一个cd结构体,注意它是不同于struct __CodeDirectory,是ppl内部使用的一个结构体。
MRS X8, #0, c13, c0, #4LDR W8,CBZ W8, loc_FFFFFFF0097FDADCSTLR XZR,BL _pmap_get_free_ppl_pageCBZ X0, loc_FFFFFFF0097FD648MOV X22, X0 ; new_cd_pageBL _phystokvMOV W1, #0x4000 ; size_tBL _bzero ; bzero(new_cd_page, 0x4000)MOV X0, X22BL _phystokvMOV X28, X0MOV X27, #0MOV X9, #0 ; next_page = 0MOV X22, X0 ; current_page = new_cd_pageSTR X19,__PPLTEXT:__text:FFFFFFF0097FD534 ; current_page += 0x70MOV W8, #0x91
如果_pmap_cs_cd_free_list为空,则调用_pmap_get_free_ppl_page申请一个新的物理页,对其进行初始化,可以看到每个cd结构体的大小为0x91。
MOV X10, X22 ; tmp_page = current_pageSTR X9, [X22],#0x70 ; *(current_page->next) = next_pagecurrent_page += 0x70CMP X27, #0CSEL X27, X10, X27, EQ ; if (x27 == 0)first_page = tmp_pageMOV X9, X10 ; next_page = current_pageSUBS W8, W8, #1 ; w8 -= 1B.NE loc_FFFFFFF0097FD53C ; while (w8 != 0)
循环对每个cd的next字段进行设置,同样第一个cd是保留不用的,next字段设置为0xDEADEAD0A110CED。
MOV X2, X25 ; sizeMOV X3, X24BL _hashADD X0, SP, #0xD0+__dst ; __dstADD X1, SP, #0xD0+__src ; __srcMOV W2, #0x14 ; __nBL _memmove ; memmove(new_hash, x1, 0x14)
从csblob结构体的第2个参数起始位置算hash值。如果这个hash值在静态trustcache或动态trustcache中,还要做进一步合法性检查。
STP WZR, W19, [X28,*(new_cd_page + 0x58) = flagMOV W8,STR X8, [X28,STR XZR, [X28,ADD X0, X28,STR W26, [X28,ADD X1, SP,MOV W2,BL _memmove ; memmove(new_cd_page + 0x5c, new_hash, 20)
将算出来的hash值保存在cd结构体的0x5c处。
ADR X8, _pmap_cs_registered_cdsNOPSTR X28, [X8] ; _pmap_cs_registered_cds = new_cd_pageADR X0, _pmap_cs_registered_cdsNOPMOV X1, X28BL_pmap_cs_code_directories_RB_INSERT_COLOR
_pmap_cs_registered_cds变量保存的是cd结构体的红黑树,如果它为空,直接将cd结构体设为root节点,否则调用_pmap_cs_code_directories_RB_INSERT_COLOR将其插入到红黑树中。
3.3.2 zalloc只读内存保护
3.3.2.1 zalloc_ro_mut
__attribute__((noinline))voidzalloc_ro_mut(zone_id_t zid, void *elem, vm_offset_t offset,const void *new_data, vm_size_t new_data_size){zalloc_ro_mut_validate_src(zid, elem, (vm_offset_t)new_data,new_data_size);pmap_ro_zone_memcpy(zid, (vm_offset_t) elem, offset,(vm_offset_t) new_data, new_data_size);}
zalloc_ro_mut函数用于将指定内存拷贝到只读内存,pmap_ro_zone_memcpy函数请求的是ppl中对应的服务函数,我们以最新的ios16为例进行逆向分析。
pmap_ro_zone_memcpy_ppl ; DATA XREF:MOV X20, X4MOV X22, X3MOV X23, X2MOV X21, X1MOV X25, X0ADD X24, X2, X1MOV X0, X24 ; va + offsetBL kvtophys_nofail
首先调用kvtophys_nofail将va + offset转为物理地址pa。
MOV X19, X0 ; paADRP X8, #vm_first_phys@PAGELDR X8, [X8,#vm_first_phys@PAGEOFF]ADRP X9, #vm_last_phys@PAGELDR X9, [X9,#vm_last_phys@PAGEOFF]CMP X8, X0CCMP X9, X0, #0, LSB.LS loc_FFFFFFF008498A94 ; vm_first_phys < pa < vm_last_phys
接着判断物理地址pa是否在合法地址范围内。
CBZ X22, loc_FFFFFFF008498A78 ; new_data == NULLCBZ X20, loc_FFFFFFF008498A78 ; new_data_size == 0
判断new_data是否为空, new_data_size是否为0。
MOV X0, X25MOV X1, X21MOV X2, X23MOV X3, X22MOV X4, X20BL pmap_ro_zone_validate_element ; (zid, va, offset, new_data, new_data_size)
调用pmap_ro_zone_validate_element函数做参数检查,在稍后会详细分析。
MOV X0, X19MOV X1, X21MOV X2, X20BL pmap_ro_zone_lock_phy_page ; (pa, va, offset)MOV X0, X19 ; vm_offset_tBL _ml_static_ptovirt_0MOV X1, X22 ; __srcMOV X2, X20 ; __nBL _memmove ; (pa, new_data, new_data, size)
可以看到,ppl直接使用memmove将目标内存拷贝进va对应的物理内存。Ppl并没有做请求来源的验证,这导致攻击者可以利用rop等技术直接调用此服务函数,将readonly内存改写为其他的内容。
接着,我们在仔细分析pmap_ro_zone_validate_element函数。
pmap_ro_zone_validate_element ; CODE XREF: pmap_ro_zone_bzero_ppl+6C↑pADDS X8, X3, X4B.CS loc_FFFFFFF0084946E0 ; new_data + new_data_size < new_dataMOV X3, X4B pmap_ro_zone_validate_element_dst ; (zid, va, offset, new_data_size)
首先判断new_data + new_data_size是否溢出,然后调用pmap_ro_zone_validate_element_dst。
pmap_ro_zone_validate_element_dst ; CODE XREF:ADRL X9, zone_ro_elem_sizeADD X8, X9, W0,UXTW#3LDR W8, [X8,#4] ; elem_size = zone_ro_elem_size[zid]ADRL X10, zone_info.zi_ro_rangeADD X11, X10, #8LDP X10, X11, [X10] ; x10 = start; x11 = endCMP X10, X1CCMP X11, X1, #0, LSB.LS loc_FFFFFFF008494928 ; x10 < va < x11
判断va是否在合法地址范围内,Readonly内存是从zone_info.zi_ro_range专有内存块分配的。
MOV W10, W0LSL X10, X10, #3LDR W9, [X9,X10] ; elem_size1 = zone_ro_elem_size[zid << 3]AND W10, W1, #0x3FFF ; va &= 0x3fffMOV W11, #0x4000SUB W10, W11, W10 ; va = 0x4000 - vaMUL W10, W9, W10 ; va *= elem_size1CMP W10, W9C. CS loc_FFFFFFF008494928 ; va > elem_size1
判断va是否跨page。
UBFX X9, X1, #0xE, #0x20 ; ' ' ; index = (uint43_t)(va >> 0xe)ADRP X10, #qword_FFFFFFF0077ED1D0@PAGE ;zone_info.zi_meta_baseLDR X10,LSL X9, X9, #4LDRH W9,AND W9, W9, #0x3FF ; zm_index = (zone_info.zi_meta_base[index << 4] & 0x3ff)CMP W9, W0B.NE loc_FFFFFFF008494928 ; meta->zm_index != zid
判断va对应的Meta指向的zm_index是否与参数zid相等。
SUB X9, X8, X2CMP X9, X3C. CC loc_FFFFFFF0084948D0 ; elem_size - offset > new_data_sizeCMP X8, X2B.LS loc_FFFFFFF0084948FC ; elem_size < offsetLDP X29, X30, [SP,#0x30+var_s0]ADD SP, SP, #0x40 ; '@'RETAB
3.3.2.2 zalloc_ro_mut_atomic
__attribute__((noinline))uint64_tzalloc_ro_mut_atomic(zone_id_t zid, void *elem, vm_offset_t offset,zro_atomic_op_t op, uint64_t value){value = pmap_ro_zone_atomic_op(zid, (vm_offset_t)elem, offset, op, value);return value;}
3.3.2.3 zalloc_ro_clear
voidzalloc_ro_clear(zone_id_t zid, void *elem, vm_offset_t offset, vm_size_t size){pmap_ro_zone_bzero(zid, (vm_offset_t)elem, offset, size);}
pmap_ro_zone_bzero_ppl ; DATA XREF:BL pmap_ro_zone_validate_element ;MOV X0, X19MOV X1, X21MOV X2, X20BL pmap_ro_zone_lock_phy_pageMOV X0, X19 ; vm_offset_tBL _ml_static_ptovirt_0MOV X1, X20 ; size_tBL _bzero
逻辑与前面类似,只是调用了bzero。