导语:一、概述 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 **)®_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无法保护的内存上。