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

41yf1sh 嘶吼专业版

一、概述

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, w190xfffffff0071322f8      0801150b       add w8, w8, w210xfffffff0071322fc      e9370032       orr w9, wzr, 0x3fff          // PAGE_MASK0xfffffff007132300      0101090b       add w1, w8, w90xfffffff007132304      7c8dfe97       bl sym.func.fffffff0070d58f4 // CleanPoC_DcacheRegion_Force0xfffffff007132308      e8f641f9       ldr x8, [x23, 0x3e8]0xfffffff00713230c      1aed07b9       str w26, [x8, 0x7ec]         // rRORGNLOCK = 1;0xfffffff007132310      df3f03d5       isb0xfffffff007132314      73f21cd5       msr s3_4_c15_c2_3, x19       // ARM64_REG_KTRR_LOWER_EL10xfffffff007132318      95f21cd5       msr s3_4_c15_c2_4, x21       // ARM64_REG_KTRR_UPPER_EL10xfffffff00713231c      5af21cd5       msr s3_4_c15_c2_2, x26       // ARM64_REG_KTRR_LOCK_EL10xfffffff007132320      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_TEXTMem:    0xfffffff005f5c000-0xfffffff006dd0000   File: 0x0e40000-0x1cb4000   r-x/r-x __PLK_TEXT_EXECMem:    0xfffffff006dd0000-0xfffffff007004000   File: 0x1cb4000-0x1ee8000   r--/r-- __PLK_DATA_CONSTMem:    0xfffffff007004000-0xfffffff007078000   File: 0x0000000-0x0074000   r-x/r-x __TEXTMem:    0xfffffff007078000-0xfffffff0070d4000   File: 0x0074000-0x00d0000   rw-/rw- __DATA_CONSTMem:    0xfffffff0070d4000-0xfffffff00762c000   File: 0x00d0000-0x0628000   r-x/r-x __TEXT_EXECMem:    0xfffffff00762c000-0xfffffff007630000   File: 0x0628000-0x062c000   rw-/rw- __LASTMem:    0xfffffff007630000-0xfffffff007634000   File: 0x062c000-0x0630000   rw-/rw- __KLDMem:    0xfffffff007634000-0xfffffff0076dc000   File: 0x0630000-0x0664000   rw-/rw- __DATAMem:    0xfffffff0076dc000-0xfffffff0076f4000   File: 0x0664000-0x067c000   rw-/rw- __BOOTDATAMem:    0xfffffff0076f4000-0xfffffff007756dc0   File: 0x067c000-0x06dedc0   r--/r-- __LINKEDITMem:    0xfffffff007758000-0xfffffff0078c8000   File: 0x1ee8000-0x2058000   rw-/rw- __PRELINK_DATAMem:    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 14LEXT(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, #1msr     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无法保护的内存上。

©著作权归作者所有:来自51CTO博客作者mob604756ebed9f的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. (unix domain socket)使用udp发送>=128K的消息会报ENOBUFS的错误
  2. 将uboot,kernel,rootfs下载到开发板上
  3. Linux内核 自旋锁spin lock,教你如何用自旋锁让ubuntu死锁
  4. 内核窥探|在kernel中的链表,其他的链表真的弱爆了
  5. 手把手教Linux驱动7-内核互斥锁
  6. Linux I2C内核架构分析,基于三星I2C控制
  7. 第10部分- Linux ARM汇编 寻址方式
  8. 第9部分- Linux ARM汇编 语法
  9. 第6部分- Linux ARM汇编 指令集概要

随机推荐

  1. Android入门
  2. 精灵游戏实现
  3. Android中view重绘问题
  4. [Android]笔记19:RatingBar的功能与用法
  5. Android NDK学习笔记
  6. Android实现自动填充验证码
  7. Android修改主机名和IP地址问题
  8. Android(安卓)pulltorefresh上拉下拉刷新
  9. Android(安卓)开发最佳实践
  10. android 升级包制作