导语:一、概述 KTRR,即“内核文本只读区域(Kernel Text Readonly Region)”的简称。 本文将详细分析在Apple A10芯片中开始使用的新机制,并研究这一新机制如何防范运行时iOS内核修改。在旧版本的芯片中,是通过加载位于EL3的监视器程序来实现

一、概述

KTRR,即“内核文本只读区域(Kernel Text Readonly Region)”的简称。

本文将详细分析在Apple A10芯片中开始使用的新机制,并研究这一新机制如何防范运行时iOS内核修改。在旧版本的芯片中,是通过加载位于EL3的监视器程序来实现这一目的,这种方法正如xerub在“Tick Tock”中详述的那样,存在缺陷并且可以被绕过。

在某些相关分析文章中,大家可能会看到“内存映射寄存器(Memory-mapped Registers)”一词,为了避免混淆,我不会使用这一术语,并作出定义如下:

“寄存器”表示实际意义上的寄存器,例如arm64 msr指令访问的寄存器。每个CPU内核中都有这些寄存器的副本,当内核进入休眠状态后,它们保存的值将会丢失。

“MMIO(内存映射I/O)”表示可用于与SoC上的设备进行交互的物理地址的一部分。与常规RAM一样,该部分对于整个SoC来说都是全局的,并且由于这是一个单独的设备,所以任何内核在进入休眠状态后其中存储的值都不会丢失。

二、新引入的硬件特性

除了完全放弃EL3(只留下EL1和EL0)之外,A10芯片还引入了两个新的硬件特性,共同作为KTRR机制的基础。

RoRgn

RoRgn属于MMIO,由“start(开始)”、“end(结束)”和“lock(锁定)”三个字段组成。在进行写入时,“lock”字段将锁定全部三个字段,以防止对它们进行任何修改。“start”和“end”字段中保存DRAM中的页数。在该区域内,内存控制器将拒绝任何写入操作。

XNU有3个宏,用于对其进行引用:

    #define rRORGNBASEADDR (*(volatile uint32_t *) (amcc_base + 0x7e4))
    #define rRORGNENDADDR  (*(volatile uint32_t *) (amcc_base + 0x7e8))
    #define rRORGNLOCK     (*(volatile uint32_t *) (amcc_base + 0x7ec))

KTRR寄存器

新机制中,会向每个CPU内核都添加一组寄存器,具体由3个寄存器组成,分别为“low(低)”、“high(高)”和“lock(锁定)”。其中,low和high寄存器负责保存物理地址,能够跨越可执行范围。如果CPU处于EL1且MMU已打开,则在该范围之外的任何指令获取(也就是尝试内存执行)将会失败,无论在页表中是否已经被标记为“可执行”。如果CPU处于EL0且MMU已关闭,那么这一范围将不产生任何影响。

针对上述寄存器,XNU也有相应的宏:

    #define ARM64_REG_KTRR_LOWER_EL1                        S3_4_c15_c2_3
    #define ARM64_REG_KTRR_UPPER_EL1                        S3_4_c15_c2_4
    #define ARM64_REG_KTRR_LOCK_EL1                         S3_4_c15_c2_2

IORVBAR

除了上述两个新增特性之外,芯片还有一个此前已经存在的特性,目前进行了一些设置上的调整。

IORVBAR同样属于MMIO,负责为每个CPU保存一个字段,用于指定该CPU在“reset(重置)”时开始执行的物理地址位置。这里所说的重置,基本上都是从睡眠状态唤醒时。在这里,同样存在一个锁定的机制,具体是通过向该字段中写入一个最低有效位为1的值来激活。

在A9及以前版本的CPU中,该机制被设置为TrustZone内的物理地址,同时WatchTower(KPP)也位于该位置。从A10开始,这一机制调整为XNU中的LowResetVectorBase,iBoot会根据下面这一注释中的内核入口点进行计算:

    /*
     * __start trampoline is located at a position relative to LowResetVectorBase
     * so that iBoot can compute the reset vector position to set IORVBAR using
     * only the kernel entry point.  Reset vector = (__start & ~0xfff)
     */

三、理论支撑

由于这些锁定机制的存在,上述特性看上去是无法被破坏的,然而这些机制却不会保护正在运行的内核。为了进行更深入的脆弱性分析,我们必须要看一下潜在的攻击:

1、(前提条件)上述机制正常情况下是有效的,并且能够符合预期的防护效果。

2、攻击者无法禁用任何防护机制,也无法选择不启用它们,攻击者在获得代码执行之前必须先攻破这些防护机制。

3、攻击者无法覆盖任何关键数据,因此所有的数据都需要位于RoRgn中。从理论上来看,这一点是显而易见的,可从实际上来看却有很多关键的地方容易被忽略。

4、攻击者无法修改从RoRgn进行内存映射的页表,否则攻击者就可以将这部分的内存复制到一个可以写入的位置,并修改页表指向该位置。因此,这样的页表也必须要在RoRgn中。

5、攻击者不能使用自定义的页表结构,所以映射内核中一半地址空间的转换表基址寄存器(在这里是ttbr1_el1)都是不可更改的。

6、攻击者无法对任何可执行的内存进行修改或注入,否则攻击者就可以添加指令msr ttbr1_el1, x0以获得对ttbr1_el1进行修改的能力。因此,“可执行范围”必须包含在RoRgn范围之内。

7、攻击者无法关闭MMU,因为当MMU关闭时,所有页都被认为是可执行的,攻击者就又可以注入msr ttbr1_el1, x0。MMU的状态由寄存器sctlr_el1的最低有效位控制,因此在正常操作之前,这个寄存器也是无法进行修改的。

8、在MMU启用之前,攻击者无法获得代码执行。由于CPU在从睡眠状态唤醒时会关闭MMU,这也就意味着IORVBAR必须指向RoRgn内的存储器,并且在MMU启用之前执行的任何代码都不能让其控制流由RoRgn外部的数据控制。

四、具体实践

让我们具体来看看,iOS是如何实现以上所有内容的:

1、IORVBAR是第一个被完整设置的机制,在跳转到内核的入口点之前由iBoot完成,并且会一次性地完成加载和锁定过程。

RoRgn首先会获得iBoot设置的起始值和结束值,但没有进行锁定。这一点也是必要的,因为后续会有很多只读的数据需要一次性初始化,而该过程是由内核自身完成的。在这里,我会跳过大部分的内核引导过程,在设置虚拟内存和所有常量数据(包括kexts)之后,就会到达kernel_bootstrap_thread,其中一部分读取如下:

    machine_lockdown_preflight();
    /*
    *  Finalize protections on statically mapped pages now that comm page mapping is established.
    */
    arm_vm_prot_finalize(PE_state.bootArgs);
 
    kernel_bootstrap_log("sfi_init");
    sfi_init();
 
    /*
    * Initialize the globals used for permuting kernel
    * addresses that may be exported to userland as tokens
    * using VM_KERNEL_ADDRPERM()/VM_KERNEL_ADDRPERM_EXTERNAL().
    * Force the random number to be odd to avoid mapping a non-zero
    * word-aligned address to zero via addition.
    * Note: at this stage we can use the cryptographically secure PRNG
    * rather than early_random().
    */
    read_random(&vm_kernel_addrperm, sizeof(vm_kernel_addrperm));
    vm_kernel_addrperm |= 1;
    read_random(&buf_kernel_addrperm, sizeof(buf_kernel_addrperm));
    buf_kernel_addrperm |= 1;
    read_random(&vm_kernel_addrperm_ext, sizeof(vm_kernel_addrperm_ext));
    vm_kernel_addrperm_ext |= 1;
    read_random(&vm_kernel_addrhash_salt, sizeof(vm_kernel_addrhash_salt));
    read_random(&vm_kernel_addrhash_salt_ext, sizeof(vm_kernel_addrhash_salt_ext));
 
    vm_set_restrictions();
 
 
 
    /*
    * Start the user bootstrap.
    */
    bsd_init();

我们比较感兴趣的第一个地方是machine_lockdown_preflight,它其实是rorgn_stash_range的一个包装器(Wrapper),负责抓取iBoot计算出的值,将其转换为物理地址,并存储在RoRgn内存中以供后续使用:

void rorgn_stash_range(void)
{
    /* Get the AMC values, and stash them into rorgn_begin, rorgn_end. */
 
    uint64_t soc_base = 0;
    DTEntry entryP = NULL;
    uintptr_t *reg_prop = NULL;
    uint32_t prop_size = 0;
    int rc;
 
    soc_base = pe_arm_get_soc_base_phys();
    rc = DTFindEntry("name", "mcc", &entryP);
    assert(rc == kSuccess);
    rc = DTGetProperty(entryP, "reg", (void **)&reg_prop, &prop_size);
    assert(rc == kSuccess);
    amcc_base = ml_io_map(soc_base + *reg_prop, *(reg_prop + 1));
 
    assert(rRORGNENDADDR > rRORGNBASEADDR);
    rorgn_begin = (rRORGNBASEADDR << ARM_PGSHIFT) + gPhysBase;
    rorgn_end   = (rRORGNENDADDR << ARM_PGSHIFT) + gPhysBase;
}

接下来,arm_vm_prot_finalize在它们变为只读之前,最后一次修改主内核二进制文件的页表,然后从所有常量和代码区域中删除可写标志。

在bsd_init之前,有一个对machine_lockdown的调用,这是rorgn_lockdown的包装器。这个调用看起来似乎是被#indef了,但如果我们将一些函数与其反汇编结果进行比较,会发现很明显是在这个位置调用了machine_lockdown:

void rorgn_lockdown(void)
{
    vm_offset_t ktrr_begin, ktrr_end;
    unsigned long plt_segsz, last_segsz;
 
    assert_unlocked();
 
    /* [x] - Use final method of determining all kernel text range or expect crashes */
    ktrr_begin = (uint64_t) getsegdatafromheader(&_mh_execute_header, "__PRELINK_TEXT", &plt_segsz);
 
    ktrr_begin = kvtophys(ktrr_begin);
 
    /* __LAST is not part of the MMU KTRR region (it is however part of the AMCC KTRR region) */
    ktrr_end = (uint64_t) getsegdatafromheader(&_mh_execute_header, "__LAST", &last_segsz);
    ktrr_end = (kvtophys(ktrr_end) - 1) & ~PAGE_MASK;
 
    /* [x] - ensure all in flight writes are flushed to AMCC before enabling RO Region Lock */
    assert_amcc_cache_disabled();
 
    CleanPoC_DcacheRegion_Force(phystokv(ktrr_begin), (unsigned)((ktrr_end + last_segsz) - ktrr_begin + PAGE_MASK));
 
    lock_amcc();
 
    lock_mmu(ktrr_begin, ktrr_end);
 
    /* now we can run lockdown handler */
    ml_lockdown_run_handler();
}
 
static void lock_amcc()
{
    rRORGNLOCK = 1;
    __builtin_arm_isb(ISB_SY);
}
 
static void lock_mmu(uint64_t begin, uint64_t end)
{
    __builtin_arm_wsr64(ARM64_REG_KTRR_LOWER_EL1, begin);
    __builtin_arm_wsr64(ARM64_REG_KTRR_UPPER_EL1, end);
    __builtin_arm_wsr64(ARM64_REG_KTRR_LOCK_EL1,  1ULL);
 
    /* flush TLB */
    __builtin_arm_isb(ISB_SY);
    flush_mmu_tlb();
}
0xfffffff0071322f4      8802134b       sub w8, w20, w19
0xfffffff0071322f8      0801150b       add w8, w8, w21
0xfffffff0071322fc      e9370032       orr w9, wzr, 0x3fff          // PAGE_MASK
0xfffffff007132300      0101090b       add w1, w8, w9
0xfffffff007132304      7c8dfe97       bl sym.func.fffffff0070d58f4 // CleanPoC_DcacheRegion_Force
0xfffffff007132308      e8f641f9       ldr x8, [x23, 0x3e8]
0xfffffff00713230c      1aed07b9       str w26, [x8, 0x7ec]         // rRORGNLOCK = 1;
0xfffffff007132310      df3f03d5       isb
0xfffffff007132314      73f21cd5       msr s3_4_c15_c2_3, x19       // ARM64_REG_KTRR_LOWER_EL1
0xfffffff007132318      95f21cd5       msr s3_4_c15_c2_4, x21       // ARM64_REG_KTRR_UPPER_EL1
0xfffffff00713231c      5af21cd5       msr s3_4_c15_c2_2, x26       // ARM64_REG_KTRR_LOCK_EL1
0xfffffff007132320      df3f03d5       isb

因此,在5条指令中,内核会锁定为RoRgn预先设定好的iBoot值,随后初始化,并锁定KTRR寄存器。

然后它会继续引导BSD子系统,最终创建userland和launchd进程。这也就意味着,任何基于APP、WebKit甚至是untether二进制文件的漏洞,都无法对KTRR利用。攻击者在这里需要一个bootchain漏洞利用,或者是在内核引导期间就能运行的漏洞利用。而这一点,在具有KASLR机制存在的情况下,看上去是不可能的。

2、针对这一点,我们没有太多分析,XNU只是在iOS 10中重新安排了它的段(Segment),以此来适应这样的内存布局:

Mem:    0xfffffff0057fc000-0xfffffff005f5c000   File: 0x06e0000-0x0e40000   r--/r-- __PRELINK_TEXT
Mem:    0xfffffff005f5c000-0xfffffff006dd0000   File: 0x0e40000-0x1cb4000   r-x/r-x __PLK_TEXT_EXEC
Mem:    0xfffffff006dd0000-0xfffffff007004000   File: 0x1cb4000-0x1ee8000   r--/r-- __PLK_DATA_CONST
Mem:    0xfffffff007004000-0xfffffff007078000   File: 0x0000000-0x0074000   r-x/r-x __TEXT
Mem:    0xfffffff007078000-0xfffffff0070d4000   File: 0x0074000-0x00d0000   rw-/rw- __DATA_CONST
Mem:    0xfffffff0070d4000-0xfffffff00762c000   File: 0x00d0000-0x0628000   r-x/r-x __TEXT_EXEC
Mem:    0xfffffff00762c000-0xfffffff007630000   File: 0x0628000-0x062c000   rw-/rw- __LAST
Mem:    0xfffffff007630000-0xfffffff007634000   File: 0x062c000-0x0630000   rw-/rw- __KLD
Mem:    0xfffffff007634000-0xfffffff0076dc000   File: 0x0630000-0x0664000   rw-/rw- __DATA
Mem:    0xfffffff0076dc000-0xfffffff0076f4000   File: 0x0664000-0x067c000   rw-/rw- __BOOTDATA
Mem:    0xfffffff0076f4000-0xfffffff007756dc0   File: 0x067c000-0x06dedc0   r--/r-- __LINKEDIT
Mem:    0xfffffff007758000-0xfffffff0078c8000   File: 0x1ee8000-0x2058000   rw-/rw- __PRELINK_DATA
Mem:    0xfffffff0078c8000-0xfffffff007b04000   File: 0x2058000-0x2294000   rw-/rw- __PRELINK_INFO

RoRgn保护的范围是从__PRELINK_TEXT到__LAST,可执行范围是从__PRELINK_TEXT到__TEXT_EXEC。

3、是的,对应主内核二进制文件的页表位于__DATA_CONST中,并且其名称为“ropagetable”:

/* reserve space for read only page tables */
        .align 14
LEXT(ropagetable_begin)
        .space 16*16*1024,0

4、为了使ttbr1_el1无法更改,攻击者能够ROP进入的可执行内存中不应包含指令msr ttbr1_el1, xN。然而,由于从睡眠状态唤醒后需要CPU重新初始化,因此该命令需要存在。考虑到在执行该命令时MMU仍然处于被禁用的状态,并且所有内存都是可执行的,因此在这一点上并没有实际的威胁。为解决这一问题,Apple创建了一个新的Segment/Section __LAST.__pinst(可能是受保护的指令“protected instructions”的缩写),并将所有他们认为至关重要的指令移动到这里,其中就包括msr ttbr1_el1, x0。尽管__LAST段位于RoRgn中,但它并不在可执行范围内,所以只有在MMU关闭的前提下才可以执行。

5、可执行范围必须是RoRgn范围的子集,因此要严格进行检查。

6、与ttbr1_el1相同,存在一个msr sctlr_el1, x0的实例,位于__LAST.__pinst中。

7、IORVBAR指向LowResetVectorBase,它位于__TEXT_EXEC中,是RoRgn的一部分,因此所有CPU在从睡眠状态唤醒后都会从只读内存开始。在这时,内核并不安全,因为从理论上来说,控制流在MMU启用之前仍然可以被重定向(在common_start中通过MSR_SCTLR_EL1_X0来实现),但实际上似乎没有地方可以让我们重定向控制流。即使我们通过某种方法已经取得了成功,从而可以修改ttbr1_el1和“remap”常量数据等内容,但最后还是要启用MMU,而启用了MMU就会失去修改ttbr1_el1和sctlr_el1的能力,也不能执行任何注入的代码。其原因在于,从睡眠状态唤醒后的CPU,执行的第一个任务往往就是再次锁定寄存器:

    .text
    .align 12
    .globl EXT(LowResetVectorBase)
LEXT(LowResetVectorBase)
    // Preserve x0 for start_first_cpu, if called
 
    // Unlock the core for debugging
    msr     OSLAR_EL1, xzr
 
    /*
     * Set KTRR registers immediately after wake/resume
     *
     * During power on reset, XNU stashed the kernel text region range values
     * into __DATA,__const which should be protected by AMCC RoRgn at this point.
     * Read this data and program/lock KTRR registers accordingly.
     * If either values are zero, we're debugging kernel so skip programming KTRR.
     */
 
    // load stashed rorgn_begin
    adrp    x17, EXT(rorgn_begin)@page
    add     x17, x17, EXT(rorgn_begin)@pageoff
    ldr     x17, [x17]
    // if rorgn_begin is zero, we're debugging. skip enabling ktrr
    cbz     x17, 1f
 
    // load stashed rorgn_end
    adrp    x19, EXT(rorgn_end)@page
    add     x19, x19, EXT(rorgn_end)@pageoff
    ldr     x19, [x19]
    cbz     x19, 1f
 
    // program and lock down KTRR
    // subtract one page from rorgn_end to make pinst insns NX
    msr     ARM64_REG_KTRR_LOWER_EL1, x17
    sub     x19, x19, #(1 << (ARM_PTE_SHIFT-12)), lsl #12
    msr     ARM64_REG_KTRR_UPPER_EL1, x19
    mov     x17, #1
msr     ARM64_REG_KTRR_LOCK_EL1, x17

在这里,不存在条件分支,也没有RoRgn之外的任何内存访问权限。

五、Meltdown/Spectre漏洞缓解(>=11.2版本)

通常来说,Meltdown是Spectre的一个分支,而后者是在几乎所有现代处理器中发现的一整类漏洞的统称。这些漏洞允许攻击者从CPU上运行的任何软件中获得想要的任何数据。

针对iOS内核,如果想要避免这一漏洞的产生,就必须要在进入EL0之前取消映射整个地址空间,并在返回EL1后恢复该映射。考虑到内核页表是只读的,并且能够限制无法对ttbr1_el1进行修改,我们看上去似乎无法在不破坏KTRR的情况下缓解Specter漏洞。然而,Apple却找到了一种有效的方式,我们首先需要掌握一些技术背景。

在ARMv8中,将虚拟地址转换成位于EL0和EL1的物理地址,其工作方式如下:

1、ttbr0_el1提供从0x0开始向上到某个地址的页表结构。

2、ttbr1_el1提供从0xffffffffffffffff开始向下到某个地址的页表结构。

3、中间的所有地址,都是无效或未映射的。

这两个“特定位置”都是通过tcr_el1寄存器配置的,特别是其中T0SZ和T1SZ字段(详见ARMv8参考手册,D10-2685)。具体来说,每个范围的大小都是2^(64-T?SZ)字节,其中T?SZ越大其范围就越小。由于我们是以2的多少次幂作为操作的量级,因此在该字段中增加或减少1都会使得地址范围的大小加倍或减半。因此,Apple所做的工作非常简单:

1、他们将内核的地址空间分成了两个范围,第一个只包含在EL0和EL1之间切换的最小值,第二个则包含内核的其余部分。

2、在引导时,T1SZ设置为25,因此第一个范围会映射到0xffffff8000000000,第二个范围会映射到0xffffffc000000000。为了进行比较,这里提供下未刷新的内核基址为0xfffffff007004000。

3、当切换到EL0时,T1SZ增加到26,因此第一个范围变成0xffffffc000000000,并且不再映射第二个范围,当返回时该值恢复为25。

4、执行向量(Exception Vector)将映射到这两个范围中,而vbar_el1针对二者都有效。

在这一过程中,我们发现了一个有趣的事实:tcr_el1原本只存在于__LAST.__pinst中,但随后会回到正常的可执行内存中,因为它显然并不是那么的关键。

六、脆弱性分析

再次分析我们刚刚列举的8条,我们就可以清楚地了解潜在的攻击方式,具体如下。

1、硬件保证

攻击者可能对受保护的内存进行比特位翻转攻击(Rowhammer Attack),并利用翻转后的结果。在特定内核存储可以访问的情况下,有可能通过改变其工作电压或引发电源故障,从而导致所有CPU寄存器、DRAM和MMIO出现错误。

2、禁用保护

攻击者如果能够在iBoot或更早期获得代码执行,就能够轻松启用KTRR。并且,iBoot中很可能存在这样的漏洞。

3、RoRgn中的关键数据

目前为止,唯一一次公开的KTRR绕过是由Luca Todesco进行的。XNU中有一个BootArgs结构,该结构的相应字段中保存着内核的物理地址和虚拟基址,这些字段会在由物理内存转换到虚拟内存时(也就是启用MMU的过程)使用。在iOS 10.2之前,这个结构还没有位于只读内存中,所以攻击者可能会借此来劫持控制流。与此同时,还有一个事实支持这一点:在重置时,运行的代码没有考虑到__LAST .__ pinst,并且是包含在可执行范围内。

4、RoRgn中的RoRgn页表

据我所知,这一部分都是正确配置的,因此没有对这一部分尝试攻击。

5、ttbr1_el1不可修改

指令msr ttbr1_el1, x0是唯一的,并且只存在于__LAST.__pinst中,因此这部分可能无法进行攻击。

6、Shellcode注入

由于可执行范围是RoRgn范围的一个子集,这部分似乎也是无懈可击。

7、禁用MMU

与ttbr1_el1一样,应该无法攻击。

8、重置时获取代码执行

与第2条类似,攻击者如果能在CPU从睡眠状态唤醒后、KTRR寄存器锁定前获得代码执行,那么就能够实现对其的攻击。然而,考虑到锁定KTRR寄存器是几乎任何内核要做的第一件事,这个攻击面似乎是不存在的。

如果想在MMU启用之前获得代码执行,这对大多数人来说好像是有足够的时间,但实际上似乎也不太可能。

如果上述这些都不起作用,那么我建议可以将全部注意力放在运行时可写的内存和Apple无法保护的内存上。

源链接

Hacking more

...