苹果A10及以后版本芯片KTRR原理及脆弱性分析 - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

苹果A10及以后版本芯片KTRR原理及脆弱性分析

41yf1sh web安全 2018-08-26 10:46:46
409554
收藏

导语:

一、概述

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无法保护的内存上。

  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务