Code Signing - iOS 代码段的校验机制分析

人魔七七

共 35360字,需浏览 71分钟

 ·

2021-06-11 15:03

前言

最近在几个群里都看到大家在讨论有关 iOS JIT 的一些内容,即动态生成或修改一段机器码后尝试执行,在这个过程中大家遇到了各种各样的坑,比如断开调试器后 Crash,或是 iOS 14 后的某个版本开始 Crash,产生这些问题的根本原因是 JIT Page 在执行时产生了 Page Fault,然后没有通过内核的合法性检查导致的。这里涉及到两个细节,一个是发生 Page Fault,另一个是没有通过合法性检查,前者是后者的前提,因为检查 Text 段合法性的唯一时机就发生在 Page Fault 时。

在 ARM 架构下 MMU 接管了虚拟内存的大部分工作,这导致内核很难完全介入到内存的读、写和执行逻辑中,只能在有限的时机去修改页的权限来实现对读、写和执行的 hook,这也导致为了实现完备的 Text Page 合法性检查需要组合权限控制与一些状态位,本文将分析这些机制,从而解答以下几个问题:

  1. 什么情况下会导致校验失败的 Crash?
  2. 内核实现校验的原理和过程;
  3. 为什么连接调试器和越狱环境下即使出现了 JIT 内存也不会 Crash?

整体分析

要搞清楚上面的问题,我们必须先分析内核对 Text Page CodeSign 的处理机制,对于一个包含 codesign 的 mach-o 文件而言,__text 段的每一页都会被计算一个 shasum hash 到 slot 中,例如:

当代码第一次被执行时,代码所在的页会发生缺页中断,代码所在的页会在 mach-o 加载时被标记为 code_signed,内核在 code_signed 页的缺页中断时包含了对 hash 的检查:

kern_return_t
vm_fault_enter(vm_page_t m,
    pmap_t pmap,
    vm_map_offset_t vaddr,
    vm_prot_t prot,
    vm_prot_t caller_prot,
    boolean_t wired,
    boolean_t change_wiring,
    vm_tag_t  wire_tag,
    vm_object_fault_info_t fault_info,
    boolean_t *need_retry,
    int *type_of_fault)

{
    // ...
    /* Validate code signature if necessary. */
    if (!cs_bypass &&
        VM_FAULT_NEED_CS_VALIDATION(pmap, m, object)) {
        // ...
        vm_page_validate_cs(m); // <-- page check

由于计算和对比 hash 是比较耗时的,vm_page_validate_cs 会优先检查是否满足 fastpath:

void
vm_page_validate_cs(
    vm_page_t       page)

{
    vm_object_t             object;
    vm_object_offset_t      offset;
    vm_map_offset_t         koffset;
    vm_map_size_t           ksize;
    vm_offset_t             kaddr;
    kern_return_t           kr;
    boolean_t               busy_page;
    boolean_t               need_unmap;

    object = VM_PAGE_OBJECT(page);
    vm_object_lock_assert_held(object);

    if (vm_page_validate_cs_fast(page)) {
        return;
    }
    
    // ...
    vm_page_validate_cs_mapped_slow(page, (const void *) kaddr);

这里的具体检查逻辑是:

  1. 如果一个 codesigned 页被 map 为可写,无论当前有没有修改过,在未来的某个时候都有可能被修改(且由于 MMU 会接管虚拟内存,内核无法直接感知到写入),因此这里统一当做页已经被写入数据,hash 的目的就是防止页被篡改,既然页已经被篡改了,那么就不必继续检查了,但注意这里没有直接 kill 掉进程,而是标记为已经进行过 hash 校验 (vmp_cs_validated = true),交由后续的逻辑来在合适的实际终止掉进程
  2. 如果一个页已经被标记为已进行 hash 校验 (vmp_cs_validated == true) 那就不再继续检查 hash;
  3. 除去以上两种情况,我们需要对 Text Page 做一个 hash 校验,也就是走到了 slowpath。

你可以结合上面的描述来阅读 fastpath 代码:

/*
 * vm_page_validate_cs_fast():
 * Performs a few quick checks to determine if the page's code signature
 * really needs to be fully validated.  It could:
 * 1. have been modified (i.e. automatically tainted),
 * 2. have already been validated,
 * 3. have already been found to be tainted,
 * 4. no longer have a backing store.
 * Returns FALSE if the page needs to be fully validated.
 */

static boolean_t
vm_page_validate_cs_fast(
    vm_page_t       page)

{
    vm_object_t     object;

    object = VM_PAGE_OBJECT(page);
    vm_object_lock_assert_held(object);

    // 被 map 为可写,即使未被篡改也认为已被篡改(因为未来可能被不经过内核感知的篡改)
    if (page->vmp_wpmapped && !page->vmp_cs_tainted) {
        /*
         * 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);
        page->vmp_cs_validated = TRUE;
        page->vmp_cs_tainted = 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 (page->vmp_cs_validated || page->vmp_cs_tainted) {
        return TRUE;
    }
    vm_object_lock_assert_exclusive(object);

    if (!object->alive || object->terminating || object->pager == NULL) {
        /*
         * The object is terminating and we don't have its pager
         * so we can't validate the data...
         */

        return TRUE;
    }

    /* we need to really validate this page */
    vm_object_lock_assert_exclusive(object);
    return FALSE;
}

显然第一次执行时,如果未曾修改过则不满足 fastpath,从而走到了 slowpath:

void
vm_page_validate_cs_mapped_slow(
    vm_page_t       page,
    const void      *kaddr)

{
    // ...
    /* verify the SHA1 hash for this page */
    tainted = 0;
    validated = cs_validate_range(vnode,
        pager,
        mo_offset,
        (const void *)((const char *)kaddr),
        PAGE_SIZE_64,
        &tainted);

    if (tainted & CS_VALIDATE_TAINTED) {
        page->vmp_cs_tainted = TRUE;
    }
    if (tainted & CS_VALIDATE_NX) {
        page->vmp_cs_nx = TRUE;
    }
    if (validated) {
        page->vmp_cs_validated = TRUE;
    }
}

通过调用 cs_validate_range 进行 hash 校验,当发现任何 subrange 的 hash 不符时,都会叠加一个 CS_VALIDATE_TAINTED 的状态标识已被篡改 (tainted & CS_VALIDATE_TAINTED),从而在 cs_validate_range 返回后标记页为已篡改 page->vmp_cs_tainted = TRUE

接下来我们继续回到 vm_fault_enter 的后续处理逻辑:

vm_page_validate_cs(m);

#define page_immutable(m, prot) ((m)->vmp_cs_validated /*&& ((prot) & VM_PROT_EXECUTE)*/ )
cs_enforcement_enabled = cs_process_enforcement(NULL);
// ...

/* A page could be tainted, or pose a risk of being tainted later.
 * Check whether the receiving process wants it, and make it feel
 * the consequences (that hapens in cs_invalid_page()).
 * For CS Enforcement, two other conditions will
 * cause that page to be tainted as well:
 * - pmapping an unsigned page executable - this means unsigned code;
 * - writeable mapping of a validated page - the content of that page
 *   can be changed without the kernel noticing, therefore unsigned
 *   code can be created
 */

if (cs_bypass) {
    /* code-signing is bypassed */
    cs_violation = FALSE;
else if (m->vmp_cs_tainted) {
    /* tainted page */
    cs_violation = TRUE;
else if (!cs_enforcement_enabled) {
    /* no further code-signing enforcement */
    cs_violation = FALSE;
else if (page_immutable(m, prot) &&
    ((prot & VM_PROT_WRITE) ||
    m->vmp_wpmapped)) {
    /*
     * The page should be immutable, but is in danger of being
     * modified.
     * This is the case where we want policy from the code
     * directory - is the page immutable or not? For now we have
     * to assume that code pages will be immutable, data pages not.
     * We'll assume a page is a code page if it has a code directory
     * and we fault for execution.
     * That is good enough since if we faulted the code page for
     * writing in another map before, it is wpmapped; if we fault
     * it for writing in this map later it will also be faulted for
     * executing at the same time; and if we fault for writing in
     * another map later, we will disconnect it from this pmap so
     * we'll notice the change.
     */

    cs_violation = TRUE;
else if (!m->vmp_cs_validated &&
    (prot & VM_PROT_EXECUTE)
) {
    cs_violation = TRUE;
else {
    cs_violation = FALSE;
}
// ...

这里的 cs_enforcement_enabled 在进程加载时已经通过 csflags 标记为开启,而 page_immutable 凡是经过了前面的 vm_page_validate_cs 检查的页都会被标记。

接下来的逻辑将通过组合标志位决定 cs_violation 的值,它代表当前页是不是真的非法了,我们来逐个条件分析:

  1. cs_bypass 这个 flag 来自于有 JIT 权限的内存,显然普通 app 不满足这个条件,可以忽略;
  2. m->vmp_cs_tainted 当 hash 校验未通过时标记,此时会导致 cs_violation = true;
  3. cs_enforcement_enabled 来自于进程的 csflags,普通进程都是开启校验的,因此不会影响到 cs_violation 逻辑;
  4. page_immutable(m, prot) && ((prot & VM_PROT_WRITE) || m->vmp_wpmapped),即 page 本身已经进行过 hash 校验,但是被 map 为可写,即使当前能通过校验,一个有写权限的页再接下来被修改时内核无法再感知到,也就保证不了被篡改的页无法执行,因此在这种情况下直接认为页非法
  5. !m->vmp_cs_validated && (prot & VM_PROT_EXECUTE) ,即没有进行校验的可执行页(比如不包含 codesign)也是非法的。

如果上述原因导致 cs_violation = true,则会走到 cs_invalid_page 逻辑判断是否要终止进程:

if (cs_violation) {
    /* We will have a tainted page. Have to handle the special case
     * of a switched map now. If the map is not switched, standard
     * procedure applies - call cs_invalid_page().
     * If the map is switched, the real owner is invalid already.
     * There is no point in invalidating the switching process since
     * it will not be executing from the map. So we don't call
     * cs_invalid_page() in that case. */

    boolean_t reject_page, cs_killed;
    if (map_is_switched) {
        assert(pmap == vm_map_pmap(current_thread()->map));
        assert(!(prot & VM_PROT_WRITE) || (map_is_switch_protected == FALSE));
        reject_page = FALSE;
    } else {
        if (cs_debug > 5) {
            printf("vm_fault: signed: %s validate: %s tainted: %s wpmapped: %s prot: 0x%x\n",
                object->code_signed ? "yes" : "no",
                m->vmp_cs_validated ? "yes" : "no",
                m->vmp_cs_tainted ? "yes" : "no",
                m->vmp_wpmapped ? "yes" : "no",
                (int)prot);
        }
        reject_page = cs_invalid_page((addr64_t) vaddr, &cs_killed); // <-- invalid page
    }
}

cs_invalid_page 的逻辑非常简单直接,对于 csflags 不包含特定标志位的进程直接干翻:

int
cs_invalid_page(addr64_t vaddr, boolean_t *cs_killed)
{
    struct proc     *p;
    int             send_kill = 0, retval = 0, verbose = cs_debug;
    uint32_t        csflags;

    p = current_proc();

    if (verbose) {
        printf("CODE SIGNING: cs_invalid_page(0x%llx): p=%d[%s]\n",
                vaddr, p->p_pid, p->p_comm);
    }

    proc_lock(p);

    /* CS_KILL triggers a kill signal, and no you can't have the page. Nothing else. */
    if (p->p_csflags & CS_KILL) {
        p->p_csflags |= CS_KILLED;
        cs_procs_killed++;
        send_kill = 1;
        retval = 1;
    }

    /* CS_HARD means fail the mapping operation so the process stays valid. */
    if (p->p_csflags & CS_HARD) {
        retval = 1;
    } else {
        if (p->p_csflags & CS_VALID) {
            p->p_csflags &= ~CS_VALID;
            cs_procs_invalidated++;
            verbose = 1;
        }
    }
    csflags = p->p_csflags;
    proc_unlock(p);

    if (verbose) {
        printf("CODE SIGNING: cs_invalid_page(0x%llx): "
            "p=%d[%s] final status 0x%x, %s page%s\n",
            vaddr, p->p_pid, p->p_comm, p->p_csflags,
            retval ? "denying" : "allowing (remove VALID)",
            send_kill ? " sending SIGKILL" : "");
    }

    if (send_kill) {
        /* We will set the exit reason for the thread later */
        threadsignal(current_thread(), SIGKILL, EXC_BAD_ACCESS, FALSE);
        if (cs_killed) {
                *cs_killed = TRUE;
        }
    } else if (cs_killed) {
        *cs_killed = FALSE;
    }

    return retval;
}

可以看到,如果进程的 csflags 包含了 CS_KILL 则会直接终止进程,而 CS_HARD 只是影响返回值,普通进程是包含这些 flags 的,因此如果走到这里会被直接杀死。看到这里也许你也许能猜测到为什么连接调试器和越狱环境下不会引发崩溃了。

上述逻辑主要描述了首次运行某个可执行页的时候的 codesign 校验路径,它是基于 Page Fault 驱动的,而现实中产生的非法可执行页往往不满足这个条件,我们往往是用 mmap 新建一些页,将可执行页的内容拷贝过去,随后将新页标记为 r-x 然后将待修改的虚拟地址 remap 到新页所在的地址,在这种情况下缺页中断实际发生在新页写内存时,而且最后新页同时包含了 rx 权限,那么理论上代码执行就不会再触发缺页中断了,所以看起来这种方式能躲过检查,事实上却不行,这又是为什么呢?接下来我们重点分析这个部分。

如何保证对 remap 页的追踪

既然内核无法直接干预已加载且权限正确的页的操作,那么就必须在合适的时候对权限或是页的驻留进行干预,从而保证 remap 页在执行时能够产生缺页中断,进而接受 codesign 检查,下面我们以修改已有的 A 页为例来梳理这个过程和内核的介入点。:

  1. 用 mmap 新建一个 B 页,权限为 rw-,拷贝 A 页的内容过来,随后在 B 页上进行机器码的修改,这里第一次对新建的页写入数据,会产生缺页中断
  2. 将 B 页权限重新设置为 r-x;
  3. 通过 remap 将 A 页所在的地址映射到 B 页,并将 A 页 unmap。

按照常规的思路,A 页本身是合法的,而 B 页在首次写入时也已经驻留,那么 A, B 理论上都不会再发生缺页中断,那么为什么执行 B 和将 A remap 到 B 都会触发 codesign 检查呢?其实问题出在修改 B 页的权限,在对 B 页进行 mprotect 时会导致缺页,下面我们来通过一个简单地实验观察 mprotect 引起缺页机制。

这里我采用了 xnuspy (https://github.com/jsherman212/xnuspy) 来 hook 内核的缺页中断处理函数的 vm_fault_enter 部分,hook 的内容也非常简单,通过 kprintf 打印一下产生缺页的地址和缺页中断信息:

static kern_return_t
my_vm_fault_enter(
    vm_page_t m,
    pmap_t pmap,
    vm_map_offset_t vaddr,
    vm_prot_t prot,
    vm_prot_t caller_prot,
    boolean_t wired,
    boolean_t change_wiring,
    vm_tag_t  wire_tag,
    vm_object_fault_info_t fault_info,
    boolean_t *need_retry,
    int *type_of_fault)
 
{
    kprintf("vm_fault_enter-100238 at 0x%llx!!!, typeof fault %d, prot %d, caller prot %d\n", vaddr, *type_of_fault, prot, caller_prot);
    return orig_vm_fault_enter(m, pmap, vaddr, prot, caller_prot, wired, change_wiring, wired, fault_info, need_retry, type_of_fault);
}

这里的 type_of_fault 是导致中断的原因,具体表示如下:

/* Arguments for vm_fault (DBG_MACH_VM) */
#define DBG_ZERO_FILL_FAULT   1
#define DBG_PAGEIN_FAULT      2
#define DBG_COW_FAULT         3
#define DBG_CACHE_HIT_FAULT   4
#define DBG_NZF_PAGE_FAULT    5
#define DBG_GUARD_FAULT       6
#define DBG_PAGEINV_FAULT     7
#define DBG_PAGEIND_FAULT     8
#define DBG_COMPRESSOR_FAULT  9
#define DBG_COMPRESSOR_SWAPIN_FAULT  10

这里的 prot 代表当前权限, caller prot 代表这次访问所需要的权限,按照 r=1, w=2, x=4 这样去编码。接下来是我们的实验代码:

void faultTest() {
    void *newPage = mmap(NULL, PAGE_SIZE, PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 00);
    assert(newPage != MAP_FAILED);
    printf("newPage at %p\n", newPage);
    
    // round 1: rw-
    printf("content of page %lld\n", *(uint64_t *)newPage);
    uint8_t *ptr = newPage;
    for (int i = 0; i < PAGE_SIZE / sizeof(uint32_t); i += sizeof(uint32_t)) {
        // nop
        *(uint32_t *)ptr = 0xd65f03c0;
        ptr += sizeof(uint32_t);
    }
    
    // round 2: r-x
    assert(mprotect(newPage, PAGE_SIZE, PROT_READ | PROT_EXEC) == KERN_SUCCESS);
    printf("content of page %lld\n", *(uint64_t *)newPage);
    void (*func)(void) = newPage;
    func();
    
    // round 3: rw-
    assert(mprotect(newPage, PAGE_SIZE, PROT_READ | PROT_WRITE) == KERN_SUCCESS);
    printf("content of page %lld\n", *(uint64_t *)newPage);
    ptr = newPage;
    for (int i = 0; i < PAGE_SIZE / sizeof(uint32_t); i += sizeof(uint32_t)) {
        // nop
        *(uint32_t *)ptr = 0xd65f03c0;
        ptr += sizeof(uint32_t);
    }
    
    // round 4: r-x
    assert(mprotect(newPage, PAGE_SIZE, PROT_READ | PROT_EXEC) == KERN_SUCCESS);
    printf("content of page %lld\n", *(uint64_t *)newPage);
    func();
}

我们新建一个 rw 页面,然后按照 读取和写入内容 - 执行 这个循环一共操作 4 轮,猜测一下一共会发生几次缺页中断呢,中断的类型又都是怎样的?

下面揭晓答案,我们首先开启对 vm_fault_enter 的 hook,在 iOS 的 shell 中使用 xnuspy 提供的 klog 监听 kprintf 内容,随后运行上面的代码,我们先来看 faultTest 的输出内容:

newPage at 0x104bc8000
content of page 0
content of page -2999674700040305728
content of page -2999674700040305728
content of page -2999674700040305728

接下来我们在 klog 中对 0x104bc8000 进行搜索,一共找到了 4 条日志:

vm_fault_enter-100238 at 0x104bc8000!!!, typeof fault 1, prot 3, caller prot 1
vm_fault_enter-100238 at 0x104bc8000!!!, typeof fault 4, prot 5, caller prot 5
vm_fault_enter-100238 at 0x104bc8000!!!, typeof fault 4, prot 3, caller prot 3
vm_fault_enter-100238 at 0x104bc8000!!!, typeof fault 4, prot 5, caller prot 5

可以看到第一次中断类型为 DBG_ZERO_FILL_FAULT,其他都是 DBG_CACHE_HIT_FAULT,即只有第一次是硬中断,其他都是软中断;再看 prot 正好符合这 4 轮的 (rw-) ~ (r-x) 循环,也就是说每一轮操作都发生了一次中断;最后来看 caller prot,这能确定引发中断的操作,可见第一轮是由读取引发的中断,第二轮和第四轮执行都引发了中断,而第三轮的写入也引发了中断,对照实验代码可以得到结论,当页的权限通过 mprotect 发生增减时,请求对应的权限时会引发缺页中断

有的同学可能会问,第一轮一读一写只引发了一次中断,那么如果先写后读呢,下面我们交换代码顺序再试一次:

// round 1: rw-
uint8_t *ptr = newPage;
for (int i = 0; i < PAGE_SIZE / sizeof(uint32_t); i += sizeof(uint32_t)) {
    // nop
    *(uint32_t *)ptr = 0xd65f03c0;
    ptr += sizeof(uint32_t);
}
printf("content of page %lld\n", *(uint64_t *)newPage);

这次的结果为:

vm_fault_enter-100238 at 0x10677c000!!!, typeof fault 1, prot 3, caller prot 3
vm_fault_enter-100238 at 0x10677c000!!!, typeof fault 4, prot 5, caller prot 5
vm_fault_enter-100238 at 0x10677c000!!!, typeof fault 4, prot 3, caller prot 3
vm_fault_enter-100238 at 0x10677c000!!!, typeof fault 4, prot 5, caller prot 5

可以看到变为由写入导致的中断,因此我们可以得出结论,在权限不变更时,读写权限只会触发一次缺页

到这里我们就不难理解为何 remap 不行了,因为 remap 之前我们需要将待执行的页重新 mprotect 回 r-x,这会导致下次执行的缺页中断,而这一页是没有对应的 codesign 的,在下一次执行该页时会命中 codesign 检查,这一页都没有签名,那么会导致 vmp_cs_validated = false,此时页面又包含 VM_PROT_EXECUTE,那么就会导致 cs_violation = TRUE,所以接下来就会调用到 cs_invalid_page 杀死进程。

连接调试器为何能正常工作

我们可以注意到在 cs_invalid_page 中有一段对 csflags 的判断逻辑:

int
cs_invalid_page(addr64_t vaddr, boolean_t *cs_killed)
{
    // ...

    /* CS_KILL triggers a kill signal, and no you can't have the page. Nothing else. */
    if (p->p_csflags & CS_KILL) {
        p->p_csflags |= CS_KILLED;
        cs_procs_killed++;
        send_kill = 1;
        retval = 1;
    }

    /* CS_HARD means fail the mapping operation so the process stays valid. */
    if (p->p_csflags & CS_HARD) {
        retval = 1;
    } else {
        if (p->p_csflags & CS_VALID) {
            p->p_csflags &= ~CS_VALID;
            cs_procs_invalidated++;
            verbose = 1;
        }
    }
    csflags = p->p_csflags;
    proc_unlock(p);

    if (send_kill) {
        /* We will set the exit reason for the thread later */
        threadsignal(current_thread(), SIGKILL, EXC_BAD_ACCESS, FALSE);
        if (cs_killed) {
                *cs_killed = TRUE;
        }
    } else if (cs_killed) {
        *cs_killed = FALSE;
    }

    return retval;
}

如果 csflags 中不包含 CS_HARD 和 CS_KILL,那么即使走到 invalid 逻辑也会返回正确,同时不会杀死进程。正常进程是包含这些 flag 的,而当调试器连接时,或是主动调用 ptrace 会触发对进程的 cs_allow_invalid 逻辑,这里的逻辑其实就是取消 CS_KILL 和 CS_HARD,同时添加一个 CS_DEBUGGED 标记:

int
cs_allow_invalid(struct proc *p)
{
    /* There needs to be a MAC policy to implement this hook, or else the
     * kill bits will be cleared here every time. If we have
     * CONFIG_ENFORCE_SIGNED_CODE, we can assume there is a policy
     * implementing the hook.
     */

    if (0 != mac_proc_check_run_cs_invalid(p)) {
        return 0;
    }

    proc_lock(p);
    p->p_csflags &= ~(CS_KILL | CS_HARD);
    if (p->p_csflags & CS_VALID) {
        p->p_csflags |= CS_DEBUGGED;
    }
    proc_unlock(p);

    vm_map_switch_protect(get_task_map(p->task), FALSE);
    return (p->p_csflags & (CS_KILL | CS_HARD)) == 0;
}

当然这个函数不是谁都能调用的,需要先经过 AMFI 的检查,具体的检查逻辑为:

__int64 __fastcall _proc_check_run_cs_invalid(proc *a1, __int64 a2, __int64 a3, bool *a4)
{
  AppleMobileFileIntegrity *v4; // x19
  bool *v5; // x3
  __int64 result; // x0
  bool *v7; // x3
  char has_ent; // [xsp+Fh] [xbp-11h]

  v4 = a1;
  has_ent = 0;
  AppleMobileFileIntegrity::AMFIEntitlementGetBool(a1, (proc *)"get-task-allow", &has_ent, a4);
  result = 0LL;
  if ( !has_ent )
  {
    AppleMobileFileIntegrity::AMFIEntitlementGetBool(v4, (proc *)"run-invalid-allow", &has_ent, v5);
    if ( has_ent
      || (AppleMobileFileIntegrity::AMFIEntitlementGetBool(v4, (proc *)"run-unsigned-code", &has_ent, v7), has_ent)
      || (unsigned int)_permitUnrestrictedDebugging() == 1 )
    {
      result = 0LL;
    }
    else
    {
      // AMFI Boot Arguments
      if ( cs_debug )
        IOLog("AMFI: run invalid not allowed\n");
      result = 1LL;
    }
  }
  return result;
}

正常在 Xcode 中调试时被调试进程已经包含了 get-task-allow,所以当调试器通过 ptrace 等手段调试进程时,会触发 cs_allow_invalid 取消相应的 csflag,从而躲避校验逻辑。到这里你可能会问为什么调试器要这么做,这是因为调试时常常要修改代码段,例如断点其实是将指令修改为 trap 实现的。

越狱以后为何能正常工作

对于 unc0ver 之类的 boot 后漏洞越狱,无法通过有效的手段进行 kernel patch,因此一般是通过注入到 launchd,在进程启动时取消其 csflag 来实现;而对于类似于 checkra1n 可以进行 kernel patch 的越狱,则可以直接将相应的检查代码 nop 掉。

参考资料

  1. darwin-xnu - https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/bsd/kern/kern_cs.c#L220
  2. jmpews's blog - https://jmpews.github.io/2017/08/01/pwn/HookZz%E6%A1%86%E6%9E%B6/
  3. Breaking iOS Code Signing - https://papers.put.as/papers/ios/2011/syscan11_breaking_ios_code_signing.pdf
  4. Jailed Just-in-Time Compilation on iOS - https://saagarjha.com/blog/2020/02/23/jailed-just-in-time-compilation-on-ios/
  5. xnuspy - https://github.com/jsherman212/xnuspy
浏览 93
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报